重读C++ Primer笔记
C++ Primer 5E
有符号和无符号
无符号类型和有符号类型混合运算时,有符号数会被提升至无符号类型,如果其值为负责会产生错误。
int main() { unsigned int u = 10; int i = -42; std::cout<<u+i<< std::endl; // 4294967264 if sizeof(int)==4 return 0; }
列表初始化
列表初始化过程不允许损失数据类型精度,所以下面代码中的两行无法通过编译
int main() { double d = 3.14; int i1 = d; //int i2 = { d }; //error //int i3{ d }; //error int i4(d); return 0; }
列表初始化可以用于直接初始化返回值。
函数内变量初始值
函数体内的基本类型局部变量如果没有初始化,则其值是未定义的。
constexpr与指针
constexpr修饰指针时,只能被初始化为nullptr、0或是在内存中位置固定的对象。而且 constexpr int *q中constexpr修饰的是*,而不是int,这和const是不同的。
类型别名(type alias)
和typedef作用相同,但可以定义带模板参数的类型:
http://en.cppreference.com/w/cpp/language/type_alias
template<class T> using Vec = vector<T, Alloc<T>>;
auto和decltype
除了用法不同外,这两个的区别是,auto会去除顶层const(int *const中的const)和引用,而decltype不会。
decltype的结果和表达式的形式密切相关,decltype((variable))返回的结果永远是一个引用。对于下面数组两者的结果为
int a[10]; auto b(a); //int *b; decltype(a) c; //int c[10];
默认实参的声明
默认实参不能重复定义,但可以进行增补:
int Fun(int a, int b, int c = 10); int Fun(int a, int b, int c = 10); //error int Fun(int a, int b = 10, int c); //ok
类作用域和定义在类外部的成员
struct Foo { typedef int T; T Test(); }; T Foo::Test(){ return T();}//error 返回值类型T未知 Foo::T Foo::Test(){ return T();}//ok auto Foo::Test() -> T { return T();}//C++11,ok
上述代码中,Foo::Test中的Foo::从Test开始生效,但前面的返回值不在此范围内,所以必须使用Foo::T的方式引用,或是使用C++11的返回值类型后置语法。
Literal Classes
Literal Classes是一种其实例可以成为constexpr对象的类型,成为Literal Classes的条件是:
1、 数据成员必须都是字面值类型(Literal type)
2、 类至少有一个constexpr构造函数。
3、 如果一个数据成员含有类内初始值,则:
a) 内置类型成员的初始值必须是一条常量表达式
b) 其他类型成员必须使用它自己的constexpr构造函数。
4、 类必须使用默认的析构函数。
Literal Classes可以包含非constexpr的函数,也可以作为普通类型使用,但作为constexpr常量时,只能使用其constexpr的构造函数和成员函数。
Literal Classes可以让constexpr常量在编译时初始化,参与编译时的优化,而不是像static那样推迟到程序启动时,这使得constexpr常量对象可以被放在程序的资源中。
Lambda表达式与捕获
Lambda表达式中值捕获后的变量在lambda中是带const修饰的,在参数列表后加mutable可以消除该效果,变成和普通函数一样可以修改传入的参数的值。
class Foo { public: int value; Foo(int v) : value(v){} Foo(const Foo &o) : value(o.value){ printf("Copy init\n"); } int inc() { return ++value; } int val() const { return value; } }; int main() { Foo a(3); auto f1 = [=]() -> int { return a.val();}; printf("%d\n", a.value); auto f2 = [=]() mutable -> int { return a.inc();}; printf("%d\n", a.value); //auto f3 = [=]() -> int { return a.inc();}; //error return 0; }
运行结果为
Copy init Call val 3 Copy init Call inc 3
移动迭代器
一般的迭代器解引用返回左值,可以使用make_move_iterator将普通迭代器转换为移动迭代器,该迭代器解引用时返回右值。
右值和左值引用成员函数
和const一样,类的成员函数可以通过在参数列表后面添加 &或&&来限制该函数只能在左值或右值对象上调用,并可以通过这种方式进行重载,如
class Foo{ public: Foo sorted() &&; Foo sorted() const &;//不能写做& const }
当成员函数中的重载函数有一个使用了&或&&时,其他重载函数也必须使用,如
class Foo{ public: Foo sorted() &&; Foo sorted() const ;//error };
Static_cast与右值引用
Static_cast可以将左值引用转换为右值引用。
OOP相关
final关键字
final关键字可以阻止类被继承
class Foo final {};
继承的构造函数
C++11加入的,详见C++ Primer 5E 15.7.4节
一个类只能继承它的直接基类的构造函数,而且不能继承默认、拷贝和移动构造函数。语法为:
class Foo: public class Base { public: using Base::Base; //继承Base的构造函数 int Fun(); };
其作为对于继承每个构造函数,编译器都为之生成对应的派生类构造函数,其形参列表与基类的构造函数完全相同。形如
derived(parms): base(args){}
如果派生类含有自己的数据成员,则这些成员将被默认初始化。(参见C++ Primer 5E 7.1.4节)
和普通成员的using声明不同,一个构造函数的using声明不会改变该构造函数的访问级别。而且using语句不能额外指定explicit或constexpr,生成的构造函数继承对应的基类构造函数的相应属性。
如果一个基类构造函数含有默认实参,这些默认实参不会被继承,而是生成多个构造函数,每个构造函数分别省略掉一个含有默认实参的形参。即基类的构造函数
int Foo(A a, B b = b0, C c = c0);
在基类中会生成下面三个构造函数
int Foo(A a, B b, C c); int Foo(A a, B b); int Foo(A a);
继承的构造函数不会作为用户定义的构造函数来使用,因此如果一个类只含有继承的构造函数,则它也将拥有一个编译器自动合成的默认构造函数。
在多继承的情况下,允许从多个直接基类继承构造函数,当有冲突出现时,必须定义派生类自己对应的版本。例如:
struct Base1 { Base1() = default; Base1(const std::string&); Base1(int); }; struct Base2 { Base2() = default; Base2(const std::string&); Base2(const char*); }; struct D1 : public Base1, Base2 { using Base1::Base1; using Base2::Base2; D1(const std::string&);//这是必须的 D1() = default;//定义上一行后编译器不再产生默认的无参构造函数 };
派生类向基类转换的可访问性
只有当派生类D公有地继承自基类B时,才能使用派生类向基类的转换(指针、引用)。
友元与继承
友元关系不能被继承,基类的友元在访问派生类的成员时不具有特殊性,派生类的成员也不能随意访问基类的成员。但基类的友元可以访问派生类中基类的部分。如
class Base { protected: int prot_mem; friend class Pal; }; class Sneaky : public Base { int j; }; class Pa1 { int f(Base b) { return b.prot_mem;} //ok //int f2(Sneaky s) { return s.j; }// error int f3(Sneaky s) { return s.prot_mem;} //ok }; class D2 : public Pal { //int mem(Base b) { return b.prot_mem; }//error };
虚析构函数与移动构造函数
虚析构函数会阻止编译器为其生成默认的移动构造函数,即使使用=default指定也不会生成。
构造和析构过程中调用虚函数
在构造函数和析构函数中,调用虚函数时,调用的是该构造函数/析构函数所在的继承层级中所定义的虚函数。即,在B-D1-D2这样的继承链中,D1的构造函数和析构函数调用虚函数时调用的是D1自己或B的虚函数版本,而不会是D2的。
多重继承与指针转换
多继承时,派生类指针向非第一个基类的转换会导致指针的值发生变化。
class Base1 { int v1; }; class Base2 { int v2; }; class D1 : public Base1, Base2 { int v3; }; int main() { D1 *d1 = new D1(); std::cout << ((int)(Base1*)d1) - ((int)d1) << std::endl; //0; std::cout << ((int)(Base2*)d1) - ((int)d1) << std::endl; //sizeof(int) return 0; }