首页 > 代码库 > 重读C++ Primer笔记

重读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;
}