重拾 c++

C++由来已久,也做了好几年了,但是还是有好多功能没用到过,并且随着 c++11的推出,增加了不少新功能,这里开始做笔记。

强枚举类型

普通的 enum 类型是弱类型的,默认是整形,并且可以发生隐式类型转换,c++ 也有提供强类型的枚举类型:

1
2
3
4
5
6
7
8
9
10
enum class MyEnum	// class 表示强枚举类型
{
EnumValue1,
EnumValue2 = 10,
EnumValue3
}
// 必须使用作用域操作符
MyEnum value = MyEnum::EnumValue1;
// 此外,枚举值不会自动转换成整数,下面的代码是不合法的:
if (MyEnum::EnumValue3 == 11){...}

默认情况下,枚举值的基本类型是整型,可以采用以下方式加以改变:

1
2
enum class MyEnumLong: unsigned long
{...}

std::array

常用的如int arr[]这种形式是来自于 C 的数组形式,C++ 有一种==大小固定==的特殊容器 std::array,来自于头文件,基本是对 C 风格的数组进行了简单包装,但是具有以下好处:

  • 总是知道自身的大小
  • 不会自动转换为指针,从而避免某些类型的 bug。
  • 具有迭代器,可以方便的遍历元素。

示例代码:

1
2
3
array<int, 3> arr = {9, 8, 7};
cout << "Array size = " << arr.size() << endl;
cout << "Element 2 = " << arr[1] << endl;

基于区间的 for 循环

这种循环允许方便地迭代容器中的元素,这种循环类型可以用于 C 风格的数组、初始化列表,也可以用于具有返回迭代器的 begin() 和 end()函数的类型。

1
2
3
4
5
std::array<int, 4> arr = {1, 2, 3, 4};
for(int i : arr){
// 这里迭代时每次都制作了一个 int 类型的副本,可以考虑使用引用。
cout << i << endl;
}

替代的函数语法

自 C++11 以来,通过拖尾返回类型(trailing return type)支持一种替代的函数语法,这种语法在指定模板函数的返回类型时非常有用。

1
2
3
4
5
6
7
8
9
10
auto func(int i) -> int
{
return i + 2;
}
// 该函数的返回类型放在行尾的箭头后面,具体调用的方法和普通函数语法相同,
// main()函数也可以使用这种替代语法:
auto main() -> int
{
return 0;
}

关键字 auto 和 decltype

关键字 decltype 把表达式作为实参,计算出该表达式的类型。例如:

1
2
int x = 123;
decltype(x) y = 456;

这里要强调的是,auto 推断表达式的类型,会去除引用限定符和 const 限定符,而 decltype 没有去除这些限定符。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const string message = "Test";
const string& foo()
{

return message;
}
auto f1 = foo();
// 因为 auto 去除了引用和 const 限定符,所以这里实际建立了message 的一个副本。
// 如果希望 f1 是一个 const 引用,就可以明确将它建立为一个引用,
// 并标记为 const。如:const auto& f1 = foo();

// 另一个解决办法是使用 decltype,它没有去除任何限定符
decltype(foo()) f2 = foo();
// 这里 f2 是 const string& 类型,但是这么写比较麻烦,要写两次 foo(),C++14提供了如下解决方法:
decltype(auto) f3 = foo();

智能指针

为了避免常见的内存问题,应使用智能指针代替通常的 C 样式指针。C++ 中有三种智能指针:std::unique_strstd::shared_ptrstd::weak_ptr,他们都在头文件<memory>中定义。

unique_ptr

unique_ptr 类似于普通指针,但在 unique_ptr 超出作用于或被删除时,会自动释放内存或资源,它只属于它指向的对象,优点是发生异常情况必须释放资源时,它简化了代码(自动帮你释放了资源)。

1
2
3
4
auto anEmployee = std::make_unique<Employee>;// make_unique 用来创建 unique_ptr

// 如果编译器还不支持 C++14,可以使用如下形式代替:
std::unique_ptr<Employee> anEmployee(new Employee());
shared_ptr

shared_ptr 允许数据的分布式“所有权”,每次指定 shared_ptr 时,都递增一个引用计数,指出数据又多了一个“拥有者”。shared_ptr 超出作用于或者被删除时,就递减引用计数。当引用计数为0时,就表示数据不再有任何拥有者,于是释放指针引用的对象。
unique_ptr 类似,可以使用 make_shared来创建shared_ptr

weak_ptr

使用weak_ptr可以观察shared_ptr,而不会递增或递减所链接shared_ptr的引用计数。

总的原则就是,默认情况下使用 unique_ptr,如果有共享需求就使用shared_ptr

==注意新的 c++标准已经废弃了auto_ptr,所以不要再使用它。==

对 std::string 的几点补充

std::string 字面量

当使用 auto 来表明字符串常亮时,如果希望 auto 推断出的是 std::string 类型,可以使用如形式:

1
2
auto string1 = "Hello World"; // auto 被推断为 char* 类型
auto string2 = "Hello world"s; // auto 被推断为 std::string 类型。
类型转换函数

std 命名空间包含很多辅助函数用于字符串和数值之间的转换,如:

1
2
3
4
5
6
7
8
9
10
// to_string 的各种重载
string to_string(int val);
string to_string(unsigned val);
...

// sto* 系列函数
int stoi(const string& str, size_t* idx=0, int base=10);
long stol(const string& str, size_t* idx=0, int base=10);
...
// idx 接收第一个为转换的字符的索引(从第几个字符开始转换)。
原始字符串字面量

通常我们定义字面量时如果含有特殊符号,需要用到转移符(比如\"用来转移一个双引号)。原始字符串字面量可以让字符串不使用转移符而直接使用特殊符号,这个功能在操作数据库查询字符串和正则表达式时尤其有用。如:

1
2
3
4
5
string str = R"(Hello, "World"!)";
// 注意是以 R"(开头,然后以)"结束。
// 特别的,如果原始字符串字面量里面需要有)"这样和结束符一样的内容,可以使用自定义标记来代替括号作为起始和结束标记(通常选择字面量里面不会出现的标记)
// 例如我想定义字面量:"你幸(姓)福吗?",假设我使用“--”作为起始和结束标记
string str1 = R"--你幸(姓)福吗?--";

使用 delete 标记函数不可用

某些情况下我们想禁用类的拷贝构造函数或者赋值函数,有两种方式,一种是将这两个函数声明为 private,并不提供任何实现。另外一种方式就是标记为 delete,这样当外部使用这两个函数的时候,编译器就会报错。如:

1
2
3
4
5
6
class CMyClass
{
public:
CMyClass(const CMyClass& src) = delete;
CMyClass& operator=(const CMyClass& rhs) = delete;
}

在类中使用引用成员变量

在类中使用引用成员变量,必须注意只能在构造函数的初始化列表或者拷贝构造函数的初始化列表中初始化这个引用。

mutable 的使用

mutable 是为了告诉编译器在 const 类型的函数中,允许改变这个值,用法如下:

1
2
3
4
5
6
7
// 假设以下代码在某个类中
int CMyClass::getValue() const
{
value++;
return value;
}
mutable int value;

typedef的另一种实现方法

using string = std::string;等效于typedef std::string string;

使用 final 来禁用继承和重写

C++允许将类标记为 final,当继承这个类的时候,编译器就会报错,格式为:class CMyClass final{}
同理,也可以将函数标记为 final,这样子类就无法重写虚函数,格式为:virtual void MyMethod() final;
这样当子类出现virtual void MyMethod() overide;时,就会报错。

重写基类的方法一定要加上 override

override 告诉编译器,我这个函数就是重写父类的,如果父类没有匹配的虚函数,那么就报错。如果没有加上 override,那么当父类没有这个函数的时候,就是在排上类中增加了一个新的虚函数了。

使用基类的构造函数构造派生类对象

有时候基类提供了带参数的构造函数,目的是让派生类始终能用到这个构造函数,并提供相应的参数来构造对象。这种情况下可以在派生类中显示的继成基类的构造函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CBase
{
public:
CBase(int i)mValue(i){}

protected:
int mValue;
}

class CDriverd: public CBase
{
public:
using CBase::CBase;
}

// 这样就可以直接调用基类形式的构造函数来构造子类对象了
CDriverd obj(1);

我在 vs2013上测试这个功能时,发现始终调用的是派生类的拷贝构造函数,比如上述例子中,编译器提示 CDriverd(const CDriverd&)的第一个参数类型 int 无法转换成 const CDriverd& 类型。即使我把拷贝构造函数禁用掉,也一样。暂时还不知道为什么。

另外使用这个特性要注意,显示继承只能继承全部,不能要求之继承某一个。

重写具有默认参数的函数,要注意默认参数不会被继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Super
{
public:
virtual void go(int i = 2)
{

cout << "super: i = " << i << endl;
}
}
class Sub : public Super
{
public:
virtual void go(int i = 7) override
{

cout << "sub: i = " << i << endl;
}
}

上面的示例,当使用Super 的指针或者引用指向Sub 的实例调用 go 函数时,输出内容为sub:i = 2。这是因为 C++根据描述对象的表达式类型在编译时绑定默认参数,而不是根据实际的对象类型绑定参数。所以为了避免这种情况,建议在派生类中的默认参数和基类保持一致。