开始阅读 C++ Primer,留点笔记。

2. 变量和基本类型

初始化分为复制初始化和直接初始化:
  • 复制初始化:int ival = 1024;
  • 直接初始化:int ival(1024);
初始化不是赋值:

初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。

定义和声明:
  • 定义:为变量分配存储空间,还可以为变量指定初始值
  • 声明:向程序表名变量的类型和名字
  • 使用extern来声明变量而不定义它
常量:
  • 使用const限定符来定义常量
  • const对象默认为当前文件的局部变量,必须使用extern使它转为全局变量
typedef

可以用来定义类型的同义词,e.g typedef int exam_score;

头文件

避免多重包含,定义预处理器变量来进行检查:

[codesyntax lang="cpp"]

#ifndef ITEM_H
#define ITEM_H
    // your head file stuff
#endif

[/codesyntax]

3. 标准库类型

std::string
  • 字符串字面值和std::string不是同一种类型
  • string.size()返回的是string::size_type类型
  • 和字符串字面值连接的时候,+号左右最起码要有一个string类型,e.g std::string s = "hello, " + "world"; // wrong
std:vector
  • vector的size_type需要指明该类型的定义,e.g std::vector<int>::size_type
  • vector的迭代器:std::vector<int>::iterator iter = ivec.begin();

4. 数组和指针

数组
  • 长度固定,无法改变
  • 使用字符串字面值进行数组的初始化的时候,会自动附带一个空字符(null)
指针
  • 指针保存的是另一个对象的地址:std::string str = "hello!"; std::string *ptr = &str;
  • 解引用后能获得其值:std::cout << *ptr; // hello!
  • 指针和引用的区别:
    • 引用总是指向某个对象:定义引用时没有初始化是错误的
    • 给引用赋值修改的是该引用所关联的对象的值,而给指针赋值则使得指针指向另一个对象
      • 指针:int ival = 1024, ival2 = 2048; int *ptr = &ival, *ptr2 = &ival2; ptr = ptr2; // ptr now points to ival2
      • 引用:int &ri = ival, &ri2 = ival2; ri = ri2; // assigns ival2 to ival
  • 向指针直接赋值数组,则指向数组的第一位:int ia[] = {1, 2, 3}; int *ptr = ia; // ptr points to ia[0]
 C风格字符串 ...
创建动态数组 ...

5. 表达式

sizeof
  • cha:或值为char类型的表达式,返回1
  • 引用:返回存放此引用类型对象所需的内存空间大小
  • 指针:存放指针所需要的内存大小
  • 数组:数组元素的sizeof结果 * 数组元素个数
    • 可用这个特性反向获得数组元素个数:int size = sizeof(ia) / sizeof(*ia);
new和delete
  • 使用new来动态创建对象
  • 定义变量时,必须指定数据类型和名字,而动态创建的时候,只需要类型,new表达式会返回指向新创建对象的指针
    • int *ptr = new int(1024);
  • 使用delete来撤销动态创建的对象
  • 如果指针指向的不是new动态分配出来的内存地址,则这个delete操作是非法的
    • int ival = 24; int *ptr = &ival; delete ptr; // invalid
    • int *ptr2 = new int(1024); delete ptr2; // ok
  • 悬垂指针:指针被删除,但是它还指向之前指向的内存地址,容易出错,最好在删除后再将其赋值为0,表示不指向任何内存
  • 动态分配内存容易出现的错误:
    • 内存泄漏
    • 读写已删除的对象
    • 对向指向同一个内存空间的两个指针执行删除操作
类型转换 ...

7. 函数

内联函数
  • 在函数返回类型前添加inline关键字,就会将该函数转换为内联函数,在编译的时候会在调用该函数的地方“展开”,避免了函数调用的开销。
  • 内联函数必须在头文件中定义,对编译器显示可见,而不仅仅是声明
指向函数的指针 ...

12. 类

全局作用域确定操作符 “::”,访问全局变量

[codesyntax lang="cpp"]

int height = 123;
int main() {
    std::cout << ::height;

[/codesyntax]

const构造函数是不需要的

创建类型的const对象时,运行一个普通的构造函数来初始化该const对象。

构造函数初始化列表
  • 有时构造函数初始化列表是必须的
    • 没有构造函数的类类型的成员,以及const或引用类型的成员,不关是哪种类型,都必须在构造函数初始化列表中进行初始化。因为它们没有,或者没办法使用默认的构造函数进行其自身的初始化。
    • 所谓的构造函数内赋值,只不过是在构造函数初始化结束后,进入到计算阶段的逻辑。
  • 初始化列表内的初始化顺序是按照成员在类中定义的顺序来的,而不是列表内的编写顺序来的,所以应该按成员的定义顺序来编写初始化列表
类的实例化
  • YourClass yourObj(); // 错误!你声明了一个函数
  • YourClass yourObj; // 正确!
  • YourClass yourObj = YourClass(); // 正确!
  • YourClass yourObj* = new YourClass(); // 正确!记住new返回的是一个指针
构造函数的隐式转换
  • 使用explicit关键字来抑制构造函数的隐式转换
友元 ...
static成员
  • static数据成员必须在类定义体的外部定义。不像普通数据成员,static成员不是通过构造函数进行初始化,而是应该在定义时进行初始化。

15. 面向对象编程

const
  • const对象只能调用const成员函数。但构造函数和析构函数对这个规则例外,它们从不定义为常量成员,但可被常量对象调用(被自动调用)。它们也能给常量的数据成员赋值,除非数据成员本身是常量。
  • 我们定义的类的成员函数中,常常有一些成员函数不改变类的数据成员,也就是说,这些函数是"只读"函数,而有一些函数要修改类数据成员的值。如果把不改变数据成员的函数都加上const关键字进行标识,显然,可提高程序的可读性。其实,它还能提高程序的可靠性,已定义成const的成员函数,一旦企图修改数据成员的值,则编译器按错误处理。
虚函数 virtual
  • 如果没有被声明为virtual,则该函数会被继承,但是无法被重新定义(无法在派生类中重写)。只有被声明为virtual的函数才会在派生类中被覆盖,否则即便派生类重写了该函数,也不会生效。
  • 如果派生类没有重定义virtual函数,则会使用基类的版本。
  • 虚函数在运行时会发生动态绑定。在C++中,通过基类的引用(指针)调用虚函数时,发生动态绑定。被调用的函数会根据引用(指针)的实际类型进行调用。
  • 一旦函数被定义为虚函数,则其一直为虚函数,派生类无法改变。派生类可选择是否在其前面继续添加virtual保留字,但是不会有任何影响。
  • 覆盖虚函数机制:在某些特定时候,我们可以使用作用域操作符来强制指定虚函数的特定版本
    • double d = baseP->Item_base::net_price(42.1);
    • 只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制
    • 一般用来调用基类版本的函数
静态类型和动态类型

由于继承导致对象的指针和引用具有两种不同的类型。

对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

[codesyntax lang="cpp"]

class B {}
class C : public B {}
class D : public B {}
D* pD = new D(); // pD的静态类型是它声明的类型D*,动态类型也是D*
B* pB = pD; // pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C();
pB = pC; // pB的动态类型是可以更改的,现在它的动态类型是C*

[/codesyntax]

静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

[codesyntax lang="cpp"]

class B {
    void DoSomething();
    virtual void vfun();
}
class C : public B {
    void DoSomething(); // 首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。
    virtual void vfun();
}
class D : public B {
    void DoSomething();
    virtual void vfun();
}
D* pD = new D();
B* pB = pD;

[/codesyntax]

让我们看一下,pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。

让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。

上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。

指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的。

只有虚函数才使用的是动态绑定,其他的全部是静态绑定。

当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。

[codesyntax lang="cpp"]

class B {
    virtual void vfun(int i = 10);
}
class D : public B {
    virtual void vfun(int i = 20);
}
D* pD = new D();
B* pB = pD;
pD->vfun();
pB->vfun();

[/codesyntax]

有上面的分析可知pD->vfun()和pB->vfun()调用都是函数D::vfun(),但是他们的缺省参数是多少?
分析一下,缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;同理,pB->vfun()的缺省参数应该是10。

“绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)”

继承的访问控制
  • 基类中的private成员,永远不为派生类所见
  • 公用继承(派生类: public 基类):基类中的public和protected成员保持其访问控制,继承到派生类中
  • 受保护继承(派生类: protected 基类):基类中的public和protected,全部转换为protected,继承到派生类中
  • 私有继承(派生类: private 基类):基类中的所有成员,转换为private,继承到派生类中
  • 派生类中的成员访问只能比基类更严格,而不能更宽松
  • 派生类的默认继承级别是 private
  • 结构体的默认继承级别是 public
友元关系不能继承

基类的友元对派生类的成员没有特殊访问权限。

转换与继承 ...
派生类复制构造函数 ...
派生类赋值操作符 ...
派生类析构函数

派生类析构函数不负责撤销基类对象的成员。编译器总是显示调用派生类对象基类部分的析构函数。每个析构函数只负责清楚自己的成员。基类总应该定义一个虚析构函数。

纯虚函数

在函数形参表后面加上 =0 以指定纯虚函数。无法实例化含有纯虚函数的类。

16. 模板与泛型编程

模板typename与class的区别

没有实际区别,可以互换使用。typename是标准C++的组成部分。