WRY

Where Are You?
You are on the brave land,
To experience, to remember...

0%

Cpp 知识点 汇总

保留字

const

  1. 阻止一个变量被改变,可以使用const关键字

    在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

    被const 修饰的变量,只能调用其自身的const函数,不能调用非const函数(也正是通过该特点,实现const对函数的重载)

    对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

  2. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

  3. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量(const实现对函数的重载);

  4. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为左值

    1
    2
    3
    4
    5
    const classA operator*(const classA& a1, const classA& a2);
    // operator* 的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错
    classA a, b, c;
    (a * b) = c; // 对a*b的结果赋值
    // 操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。

位置汇总

1
2
3
4
5
6
7
8
const char * const p;
// 第一个const表明不能通过p修改内容;
// 第二个const表明不能修改p

const int& func(const int& para) const{}
// 第一个const表明函数返回的引用所指向的内容不能被修改(以使其返回的值不为左值);例如,func(a) = 2; 在有const的情况下是非法的
// 第二个const表明传入的形参不能被修改;
// 第三个const表明函数是否是const的,const的函数只能被const的对象调用,non-const的函数只能被non-const的对象调用

static

  • 区分自动变量还是静态持续无链接性变量

    1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  • 区分变量或者函数的作用范围

    1. 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
    2. 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  • 在类中修饰变量和函数

    1. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;(和static修饰函数体内的变量的本质一样),可参考下文中的类中的变量

      静态非常量数据成员,只能在类外定义和初始化,在类内仅是声明而已。 why?

      • 静态常量数据成员可以在类内初始化(即类内声明的同时初始化)或者类外初始化,不可以在构造函数或者初始化列表中初始化。
      • 静态非常量数据成员只能在类外初始化。因为所有的对象只有一份该成员,所以新建的对象不应该重新对其初始化,因此初始化不能放在类内。
      • 非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化;
      • 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化;

      从初始化位置的角度:

      • 可以在类内中初始化的有const、static
      • 可以在类外中初始化的有static
      • 可以在构造函数中初始化的有non-const、non-static
      • 可以在初始化列表中初始化的有non-static

      TODO 静态常量数据成员的存储范围呢?

    2. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量;

变量

变量的描述

具体参考 C语言内存管理和指针交流研讨

内存六元组模型

M = {Address, Variable_Type, Name, Size, Value, Value_Type}

M: 申明变量系统给分配的一段内存,粗略可以分为两部分:物理属性逻辑属性,其中只有Address和Size是必不可少的,见malloc的例子。

  • 物理属性,机器知道的内容
    • Address: 在内存中的第一个字节的地址,由系统分配,一旦确定无法修改
    • Variable_Type:变量类型
    • Name:变量名称
    • Size:内存大小,必须要,且不可修改
  • 逻辑属性,程序员知道的内容
    • Value:内存表示的值
    • Value_Type:表示值的类型
      • 非数组类型,Value_Type=Variable_Type, Value是通过Value_Type去观察这段内存获得的值
      • 数组类型,指向数组里元素变量类型的指针类型,Value是数组第一个元素所处内存的第一个字节编号(等于Address)

此处数组有如下比较重要的特性,定义数组 int arr [12]; int *p:

  • 数组*(arr+n)arr[n]以及*(p+n)都是相同的含义

  • 数组指向的是数组中的第一个元素,但是对数组取址,返回的是指向整个数组的地址;因此arr+1&arr+1有本质的区别

    arr+1,是在arr地址上加了一个int大小的距离

    &arr+1,是在arr地址上加了一个数组的大小的距离

    arr代表的地址和&arr代表的地址在数值上相等,在含义类型上不相等。

  • 对数组进行sizeof计算,会得到整个数据部分的空间,即单个元素的大小乘以整个数组的长度

  • 指针指向的内存空间是通过mallocfree分配与释放的;而数组是隐式的分配和删除

区分指针数组和数组指针

1
2
int* arr[12]; // int指针的数组
int (*arr) [12] // 一个指向数组的指针

其他保留字

typedef 声明别名

typename 指定别名是类型

变量的存储位置

C++的内存分区

了解变量的存储位置前,需要先了解一下C++的内存分区,主要分为:栈、堆、全局/静态存储区、常量存储区、代码区(这些区域也是每个进程控制的区域)。

  • 栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。

  • 映射区:存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)

  • 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。

  • 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

  • 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束编译器自动释放。

    2021-10-14:具体理解存放的应该是静态常量,const static类型的变量

    在实际实验过程中,baseType的常量地址小于静态变量的地址;但是string没有体现地址小于静态变量的地址

  • 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

  • 说明:从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env

拓展了解。在一个Linux的进程的虚拟内存可以表示如下,

其中的内核虚拟内存包含内核中的代码和数据结构,其某些区域被映射到所有进程共享的物理页面,其他区域包含每个进程都不相同的数据。比如说,页表、内核在进程的上下文执行代码时使用的内核栈,以及记录虚拟地址空间当前组织的各种数据结构。

有趣的是,Linux 也将一组连续的虚拟页面(大小等同于系统中 DRAM 的总量)映射到相应的一组连续的物理页面。这就为内核提供了一种便利的方法来访问物理内存中任何特定的位置。

存储位置

通过上述的介绍,变量可以位于data,heap和stack三类空间上。

Data

data区域泛指在.data.bss的区域,比较大的特点是跟随代码块在一起,预先分配了存储空间。可以通过如下的方式在Data上创建变量:

  • 在代码块外定义的变量
  • 在代码块内定义的静态(static)变量

Heap

Heap是程序员自己管理的变量空间区域,可以通过如下的方式在堆上创建变量:

  • malloc库函数,在堆上分配一个空间;与之对应的通过free库函数进行释放
  • new操作符(operator),在调用了malloc之后,还会调用构造函数,把变量初始化出来;与之对应的通过delete函数进行析构并释放空间

mallocnew在返回结果上的差异

  • mallocnew返回的都是一个指针,malloc返回的是一个void的指针,而new返回的一个指定对象的指针

  • 若没有分配到空间,new会有一个bad_malloc的异常,而malloc会返回一个nullptr

  • new [] BaseClass的时候,会一次行分配所有的空间,再逐个初始化。在返回指针的指向的时候会有如下差异

    1
    2
    3
    4
    5
    6
    | Bookkeeping | First Object | Second Object |....
    ^ ^
    | |
    | p2: This is what is returned by new[]
    |
    p1:this is what is returned by malloc()

    因此new [] 的返回结果需要使用delete []释放,如果用delete会出现pointer being freed was not allocated的错误。

Stack

Stack是系统自动分配的,不受程序员的控制,存放的也都是局部变量,可以通过如下的方式在Stack上创建变量

  • 代码块内的非静态(non-static)变量

变量的生命周期

变量的声明周期和变量的存储位置紧密相关。具体可以分为如下几大类:

静态

此类变量全部分配在Data区域,包括全局(非静态或静态)变量局部静态变量

  • 全局(非静态或静态)变量在main函数运行前分配空间并初始化(默认初始化为0);在main函数运行后消亡

    此处的非静态和静态指对象的可见范围是文件内还是文件间,和生命周期并不相关

  • 局部静态变量在main函数运行前分配空间,在第一次被调用时初始化;在main函数运行后析构;在C++11之后,针对局部静态变量的创建还提供线程安全的保障

    提前分配空间是根据data段大小固定判断出来的

自动

作用域可以分为全局作用域、局部作用域、语句做用域、类作用域、命名空间作用域和文件作用域;(不是很理解

此类变量全部分配在Stack上,会在变量定义的时候创建,在作用域结束时消亡。主要指局部变量(自动变量)。

由程序员指定

这类变量主要在堆上,通过程序员手动创建和释放。

变量的可见范围

变量的可见范围主要针对伴随程序生命周期的变量进行描述的(其他生命周期中的变量都是在当前函数或者代码块中可见的),可以分为如下:

无链接性

局部变量,只能在当前函数或者代码块中访问。

内部链接性

全局静态变量,只能在当前文件中访问。

外部链接性

全局变量,在其他文件中也可以访问

变量的可修改性

是否被const修饰,

变量的数据类型

基本类型

复合类型

用户自定义类型

类中的变量

参考下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public:
const static int const_static_data; // 声明 不会分配空间
static int static_data; // 声明 不会分配空间
const int const_data; // 定义,但需要结合成员初始化列表
int data; // 定义
int data_1; // 定义
A(int const_data_param) :const_data(const_data_param) {
static int count = 0;
};
~A() {};
virtual void func() {}
};

const int A::const_static_data = 1000; // 定义
int A::static_data = 100; // 定义

指针

指针和引用的区别

智能指针

auto_ptr

所有权进行传递,例如p2=p1之后,p1实际上已经失效了。再调用p1将会报错,因此auto_ptr存在潜在的内存崩溃问题。

unique_ptr

所有权严格独占的模式,p2=p1的语句会报错(除非p1是一个临时右值);相比于auto_ptr更加的安全了。

  • 初始化方式和shared_ptr的初始化方式类似通过构造函数或者make_unique函数,参见shared_ptr相关信息
  • 自定义deleter函数和shared_ptr不一致,使用了模板的方式进行实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Base {
public:
Base() { cout << "Base" << endl; }
~Base() { cout << "~Base" << endl; }
};
struct MyDeleter{
void operator()(Base *p){
cout << "Delete memory[] at " << p << endl;
delete p;
}
}
int main() {
auto p = new Base(); // Base
cout << p << endl; // 0x7fd81fc059f0
unique_ptr<Base> ptr(p);
// unique_ptr<Base> ptr2 = ptr; // not allowed,这也决定了unique_ptr不能通过值传递,只能通过引用传递或者move传递
unique_ptr<Base> ptr2;
ptr2 = std::move(ptr); // ptr2原来指向的内容会被删除, ptr放弃了控制权,把其管理的内容交给了ptr2来管理
cout << ptr.get() << endl; // 0x0
cout << ptr2.get() << endl; // 0x7fd81fc059f0
unique_ptr<Base[], MyDeleter> ptr(new Base[3]); // 使用自定的deleter函数
unique_ptr<Base, MyDeleter> ptr2(new Base[3]); // 因为使用了自定义的deleter,这种表述也是ok的
unique_ptr<Base[], void (*)(Base * p)> ptr3(new Base[3], [](Base *p) { // 使用普通的函数
cout << "Delete memory[] at " << p << endl;
delete[] p;
});
unique_ptr<Base[], std::function<void(Base *)>> ptr4(new Base[3], [](Base *p) { delete[] p; }); // 也是ok的
} // ~Base
// void (*)(Base * p) 是一种数据类型,该类型是一个指针,指向一个void返回值,Base*的参数的函数
// void *(Base * p) 则是在声明一个函数

void func5(unique_ptr<Base> ptr)
{
cout << "ptr in function: " << ptr.get() << endl;
}
int main()
{
auto p = new Base();
cout << "p = " << p << endl;
unique_ptr<Base> ptr(p);
func5(move(ptr)); // 转换成右值,在函数传值的时候,ptr放弃了控制权,在函数退出时,对象随着指针的析构而释放
cout << "ptr in main: " << ptr.get() << endl;
}

shared_ptr

共享式拥有的概念,多个指针可以指向同一个资源,该资源会在最后一个指针被析构的时候释放

  • shared_ptr的所有成员函数都是线程安全的,如果多线程同时访问同一个non-const的shared_ptr,那有可能发生资源竞争(Data Race)的情况,比如改变这个shared_ptr的指向,这种情况下需要多线程同步机制。

  • 其内部包含两个指针,一个指向共享对象,另一指向共享计数区域。

  • 通过构造函数进行初始化

    • 若在构造shared_ptr的时候传入的是空指针,那么引用计数会是0

    • 在构造时只能传入指向heap的指针,而不能是一个栈的地址

    • 不允许通过一直原始指针初始化多个shared_ptr,会引起多次delete操作

    • 允许自定义删除行为

      1
      2
      3
      4
      5
      6
      int *p = new int[10];
      auto func = [](int *p) {
      delete[] p;
      cout << "Delete memory at " << p << endl;
      };
      shared_ptr<int> ptr(p, func);

      用途:删除数组,若指针指向的是一个数组,需要自定义删除(deleter)数组的行为(也可以采用C++17之后的标准支持)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Basic {
      public:
      Basic() { cout << "Basic" << endl; }
      ~Basic() { cout << "~Basic" << endl; }
      };
      int main() {
      Basic *p = new Basic[3];
      shared_ptr<Basic> ptr(p, [](Basic *p){ delete[] p; }); // 自定义删除行为
      // 或者
      Basic *p2 = new Basic[3];
      shared_ptr<Base[]> ptr(p2);
      }
  • 通过make_shared初始化

    • 接收的参数可以是一个对象,也可以是一个跟该类的构造函数匹配的参数列表

      1
      2
      auto ptr1 = make_shared<vector<int>>(10, -1);
      auto ptr2 = make_shared<vector<int>>(vector<int>(10, -1));
    • 与通过构造函数初始化不同,make_shared允许传入一个临时对象,会将该对象拷贝到heap中和原来的对象的存储空间分离开;此外通过指针修改内容不会影响到原来的对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #include<iostream>
      #include<vector>
      #include<memory>
      using namespace std;

      int main() {
      vector<int> v = {1, 2, 3};
      auto ptr = make_shared<vector<int>>(v);
      // &v = 0x7fffffffd9c0
      // ptr.get() = 0x55555556dee0
      cout << &v << endl;
      cout << ptr.get() << endl;
      // v[0] is still 1
      (*ptr)[0] = 100;
      cout << v[0] << endl;
      return 0;
      }
  • 指向一个函数

    1
    2
    3
    4
    5
    void func() { cout << "hello" << endl; }
    int main() {
    shared_ptr<void()> ptr(func, [](void (*)()) {}); // 必须自定义deleter的行为
    (*ptr)();
    }
    函数名称 含义
    make_shared 传入构造函数
    operator = 赋值函数
    use_count 返回引用计数的个数
    unique 返回是否是独占所有权( use_count 为 1)
    swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
    reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
    get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); spsp.get()是等价的

当两个shared_ptr相互指向的时候会出现循环计数的问题,从而造成内存泄漏,可以采用weak_ptr协助解决

weak_ptr

不控制对象寿命周期的智能指针

  • weak_ptr 指针通常不单独使用(因为没有实际用处),只能和 shared_ptr 类型指针搭配使用。

  • 不会改变引用计数的数量

  • weak_ptr 没有重载 *-> 运算符,因此 weak_ptr 只能访问所指的堆内存,而无法修改它

  • 成员函数如下:

    函数 作用
    operator = weak_ptr 可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值
    swap 与另外一个 weak_ptr 交换 own objetc
    reset 置为 nullptr
    use_count 查看与 weak_ptr 指向相同对象的 shared_ptr 的数量
    expired 判断当前 weak_ptr 是否失效(指针为空,或者指向的堆内存已经被释放)
    lock 如果 weak_ptr 失效,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

    weak_ptr会指向shared_ptr的数据,通过lock来获取数据,会随着shared_ptr的资源销毁而失效。

    实现的一种思路:代码链接

  • demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <memory>
    #include <iostream>
    using namespace std;
    // global weak ptr
    weak_ptr<int> gw;
    void observe() {
    cout << "use count = " << gw.use_count() << ": ";
    if (auto spt = gw.lock()) cout << *spt << "\n";
    else cout << "gw is expired\n";
    }
    int main() {
    {
    auto sp = make_shared<int>(233);
    gw = sp;
    observe(); // use count = 1: 233
    }
    observe(); // use count = 0: gw is expired
    }
  • weak_ptr解决循环引用造成的内存泄漏的问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class Parent;
    class Child;
    class Parent {
    public:
    shared_ptr<Child> childptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
    Parent() { cout << "Parent" << endl; }
    ~Parent() { cout << "~Parent" << endl; }
    };
    class Child {
    public:
    shared_ptr<Parent> parentptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
    Child() { cout << "Child" << endl; }
    ~Child() { cout << "~Child" << endl; }
    };
    int main() {
    shared_ptr<Parent> parent(new Parent());
    shared_ptr<Child> child(new Child());
    parent->childptr = child;
    child->parentptr = parent;
    } // 没有产生析构的输出;
    // 两处其实只需要一处修改成weak_ptr,打破环路即可
    // parent child
    // | |
    // V V
    // +---Parent---+ +---Child---+
    // | childptr |---------->| |
    // | |<----------| parentptr |
    // +------------+ +-----------+
    // 在堆上的指针不要是shared_ptr,因为存在于堆上,没有显示的delete操作会造成相互引用,谁都不能计数为0

c++提出了类的概念这也是c++面向对象编程的重要体现。面向对象的三大特征是

  • 封装

    把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。

    • public 成员:可以被任意实体访问
    • protected 成员:只允许被子类及本类的成员函数访问
    • private 成员:只允许被本类的成员函数、友元类或友元函数访问
  • 继承

    • 基类(父类)——> 派生类(子类);继承包括接口继承实现继承
  • 多态

    多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装继承为基础的。

    • C++ 多态分类及实现:
      1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载。是静态多态
      2. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板。是静态多态
      3. 子类型多态(Subtype Polymorphism,运行期):虚函数。是动态多态
      4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

类函数

在c++11中提出了defaultdelete关键字,用于控制编译器自动生成函数的行为。

构造函数

成员初始化列表

成员的初始化列表,在下面4中情况必须要使用。成员初始化列表不指定的变量,在进入构造函数之前会调用他们的默认构造函数,初始化空间。指定的会使用指定的方法进行初始化。变量的初始化顺序由变量在类中声明的顺序决定的。

  • 初始化一个reference member(本质上是一个const的指针)
  • 初始化一个const member
  • 调用一个base class的constructor,而他拥有一组参数
  • 调用一个member class的constructor, 而他拥有一组参数

默认构造函数

对于一个class,若没有任何 user-declared constructor,那么会有一个default constructor被implicitly声明出来,这个声明通常是trivial(无能的,在STL中会采用直接内存操作的方式初始化)的,但在以下四种情况,是non-trivial的。注意这里编译器提供的构造函数仅为了编译器的需要,并不对用户负责。

  • 带有Default Constructor的Member Class Object时,编译器声明的constructor会调用member class object的default constructor。这种情况下,即使有user-declared constructor,编译器也会按照Member Class Object定义的顺序补充调用他们的默认构造函数。
  • 带有Default Constructor的Base Class,derived class A需要合成一个constructor来调用父类的constructor,因此也是non-trival的,如此之后再继承该class A的子类,将把合成的A的constructor当作是显示声明的。
    • 此外即使A有user-declared的constructor,编译器也会扩充他们的每一个,按照顺序调用父类的default constructor。
    • 和第一种情况类似,Member Class Object的default constructor也会被扩充进来
  • 带有或者继承virtual function的class,因为需要给对象赋virtual function的地址;
  • 带有一个virtual base class的class,因为需要给对象赋virtual class对象的地址。

拷贝构造函数

有三种情况,会以一个object的内容作为另一个class object的初值,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X {...};
// 第一种情况,等号赋值
X x; // 由default constructor构造而来
X xx=x; // 显式的以一个object的内容作为另一个class object的初值,(注意这里是初始化,用到的仍是拷贝构造函数)

// 第二种情况,当作参数传入
extern void foo(X x);
void bar() {
X xx;
foo(xx); // 以xx作为foo()第一个参数的初值
}

// 第三种情况,当作返回值返回
X foo_bar(){
X *xx = new X();
return *xx;
}

拷贝构造函数和移动赋值函数之间的区别是

  • 拷贝构造函数使用的时候是对象在初始化(定义)的时候

  • 移动赋值函数使用的时候是对象已经完成初始化之后。

Copy Constructor的逻辑是把每一个内建的或者派生的data member(例如一个指针或者数组)的值,从一个objec拷贝到另一个object中;对于其中的member class object,会采用递归的方式进行memberwise initialization

若没有显示的Copy Constructor声明,就会有Implicitly Copy Constructor被声明或者定义出来,他们是不是nontrivial的,会根据他们是否具有bitwise copy semiotics来判断。不遵照bitwise copy semiotics的情况

  • class中member objects中有声明copy constructor的情况(显式声明或者被编译器合成)

  • class继承自一个base class而后者存在一个copy constructor时

  • 当class 声明了一个或者多个virtual functions时,由于可能会存在类型的在继承树上的变化,所以需要重新设定virtual table的指针

    • 例如

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class ZooAnimal{
      public:
      ZooAnimal();
      virtual ~ZooAnimal();
      virtual void animate();
      virtual void draw();
      }
      class Bear : public ZooAnimal{
      public:
      Bear();
      void animate();
      void draw();
      virtual void dance();
      }
      Bear beer;
      ZooAnimal a = beer; // 会发生切割行为,逐位复制的方式将会造成vptr错误
  • 当class 派生自一个继承串链,其中有一个或多个virtual base classes时

移动构造函数

移动构造函数通常在move函数的显式操作下执行,例如下面,c5中持有的资源将会被转移到c6中。

1
Human c6 = std::move(c5);  // 移动复制构造函数

析构函数

析构函数在变量被释放空间之前执行,用于处理一些善后的操作,例如

  • 释放堆上的内存
  • 释放持有的资源(例如锁)
  • 。。。

应用场景:

  • 正常的资源释放
  • 锁的自动释放

赋值函数

和拷贝构造或者移动构造之间的区别是:赋值函数的执行需要变量已经被定义了;而构造函数调用的时候变量不能被定义。

  • 拷贝赋值函数
  • 移动赋值函数

在实际测试过程中发现,如果没有定义移动赋值函数,会使用拷贝赋值函数来代替。

友元函数

重载运算符

不能重载的运算符只有5个: . 成员访问运算符 .* 成员指针访问运算符 :: 域运算符 sizeof 长度运算符 ?: 条件运算符

对于没有不带参数的构造函数,在定义变量的时候也不应该加括号;加了括号,反而不是一个变量的定义。

类的大小

  • 对齐原则

  • 与非静态成员变量(nonstatic data members)有关

  • 虚函数

  • 虚继承

    虚继承中如果出现钻石继承,基类只会有一个存在,此外虚拟派生类中会有一个指向基类对象的指针,这些特性都会造成类大小的计算的差异

    1
    2
    3
    4
    5
    6
    class A{};
    class B: virtual public A{};
    class C: virtual public A{};
    class D: public B, public C{
    D():A(),B(),C(){};
    }; //
  • 空类

假设结构体起始位置0,字节对齐的原则:首先找最长成员 a bytes,pack参数为 b bytes,那么有效单位长度为x=min(a, b),然后把地址按照 x 划分,然后一个个往里面填,最后不够 x ,要补足x。

类Demo

如下代码链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include <algorithm>
#include <iostream>
#include <string>

class Human {
public:
// default constructor
Human() {
log("default constructor");
age = 0;
name = nullptr;
}

// deconstructor
~Human() {
log("destructor");
if (this->name != nullptr) {
delete this->name;
}
}

// constructor with parameter
explicit Human(int age) // 禁止隐式转换
: age(age),
name(nullptr) // 成员初始化列表进行初始化
{
log("constructor with parameter");
}

// constructor with parameter
Human(char *name, int age)
: age(age) // 初始化列表进行初始化
{
log("constructor with parameter");
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
}

// copy constructor
Human(const Human &other) {
log("copy constructor");
// 保证在申请资源失败的时候不会影响到原来的资源
Human tmp(other.name, other.age);
// 和临时变量交换资源,临时变量的资源会在方法退出后被释放掉
std::swap(this->age, tmp.age);
std::swap(this->name, tmp.name);
}

// move copy Constructor
Human(Human &&other) {
// this 是一片没有被初始化的空间
log("move copy Constructor");
this->age = other.age;
this->name = other.name;
other.name = nullptr; // 此处的构造函数没有为this->name的内容进行初始化
}

// copy assignment operator
Human &operator=(const Human &other) {
log("copy assignment operator");
if (this == &other) {
return *this;
}
// 保证在申请资源失败的时候不会影响到原来的资源
Human tmp(other.name, other.age);
// 和临时变量交换资源,临时变量的资源会在方法退出后被释放掉
std::swap(this->age, tmp.age);
std::swap(this->name, tmp.name);
return *this;
}

// move assignment operator
Human &operator=(Human &&other) { // 要修改other,不能使用const修饰
log("move assignment operator");
std::swap(age, other.age);
std::swap(name, other.name);
return *this;
}

// 友元函数,可以访问类的私有变量
friend Human borth(Human &mother, Human &father);

void show() {
std::cout << "a human instance: \n"
<< "\tname: " << this->name << "\n"
<< "\tage: " << this->age << "\n";
}

private:
void log(const char *msg) { std::cout << "[" << this << "] " << msg << "\n"; }
int age;
char *name;
};

Human borth(Human &mother, Human &father) {
Human baby(0);
int i, m = mother.name ? strlen(mother.name) : 0,
n = father.name ? strlen(father.name) : 0;
baby.name = new char[m + n + 2];
for (i = 0; i < m; ++i) baby.name[i] = mother.name[i];
baby.name[i++] = ' ';
for (; i < m + n + 1; ++i) baby.name[i] = father.name[i - m - 1];
baby.name[i] = 0;
return baby;
}

int main(void) {
Human c1("c1", 18); // "c1" 的用法并不规范
// output
// [0x16fdff2d8] constructor with parameter
Human c2(c1);
// output
// [0x16fdff2c8] copy constructor
// [0x16fdff180] constructor with parameter
// [0x16fdff180] destructor
Human c4;
// output
// [0x16fdff2a8] default constructor
c4 = c2;
// [0x16fdff2a8] copy assignment operator
// [0x16fdff1a8] constructor with parameter
// [0x16fdff1a8] destructor
std::cout << "------------------------\n";
Human c5;
// output
// [0x16fdff298] default constructor
Human mother("mom", 26);
// output
// [0x16fdff288] constructor with parameter
Human father("dad", 27);
// output
// [0x16fdff278] constructor with parameter
c5 = borth(mother, father);
// output
// [0x16fdff268] constructor with parameter
// [0x16fdff298] move assignment operator
// [0x16fdff268] destructor
Human c6 = std::move(c5); // 移动复制构造函数
// output
// [0x16fdff258] move copy Constructor
std::cout << "------------------------\n";
Human c7;
// output
// [0x16fdff248] default constructor
c7 = Human();
// output
// [0x16fdff238] default constructor
// [0x16fdff248] move assignment operator
// [0x16fdff238] destructor
Human c7_o;
// output
// [0x16fdff228] default constructor
c7_o = Human();
// output
// [0x16fdff218] default constructor
// [0x16fdff228] move assignment operator
// [0x16fdff218] destructor
Human c8 = borth(c7, c7_o);
// output
// [0x16fdff208] constructor with parameter
std::cout << "<<<<<<finish >>>>>>>>\n";
// output
// [0x16fdff208] destructor c8
// [0x16fdff228] destructor c7_o
// [0x16fdff248] destructor c7
// [0x16fdff258] destructor c6
// [0x16fdff278] destructor father
// [0x16fdff288] destructor mother
// [0x16fdff298] destructor c5
// [0x16fdff2a8] destructor c4
// [0x16fdff2c8] destructor c2
// [0x16fdff2d8] destructor c1
return 0;
}

类继承

Inheritance可以让我们从base class中创造出来一个derived class,他们之间的关系是is-a的关系

继承访问控制

Cpp可以实现如下的继承

1
2
3
4
5
6
7
class Base {
.... ... ....
};

class Derived : public Base {
.... ... ....
};

Cpp的Derived Class可以使用public、protected和private三种模式来进行继承。

继承模式 特点
public inheritance Base class中的public修饰的成员保持public,protected修饰的成员保持protected
protected inheritance Base class中的public和protected修饰的成员都变成protected
private inheritance Base class中public和protected修饰的成员都变成private

Note:在Base class中使用private修饰的成员在Derived class中不可访问

虚继承

除了在继承访问控制中介绍的三种继承之外,还有一种重要的继承是虚继承;虚继承的一大特点是derived class不是在base class之后拼接,而是通过一个指针指向公共的父类。

虚函数表

动态联编通过每个类new出来的虚拟指针vptr指向一个该类所有需要动态联编函数的具体地址;

函数重载、重写、重定义

  • 重载 (Overload) :函数名相同,参数列表不同,返回值没有要求(压根就不是一个函数)
  • 重写 (Override) : 类 A 定义了一个虚函数 func,其子类 B 把函数 func 重新实现(aka, 覆盖)。可以修改函数的访问修饰符
  • 重定义 (Refine) : 类 A 定义了一个普通函数 func,其子类 B 把函数 func 重新实现

个人理解函数定位依靠命名空间函数名称参数列表是否const

重写和重定义之间的区别就是函数是否是动态绑定的

重写的函数一定是virtual并且不是static的(static是类的函数,不是对象函数,不存在多态一说)

重载

根据函数标识,重载的函数压根就不是一个函数,没啥好说的。但是在c语言中是没有这个机制的,C语言对函数的标识仅局限于函数的名称(TODO 待验证)。

重写

是C++动态多态的重要实现方式,在c++11中提出了新的关键字来增强重写的规范性

  • final:声明该虚函数不应该在被子类继承了
  • override:显示的声明这是一个重写函数,防止重写函数出现一些细节错误

使用Demo

重定义

如果相同的function在base class和derived class中都被定义了,那么一个dervied class的instance会执行derived class中的函数逻辑。但是也可以在下面的情况中执行base class的方法

  • Access Overridden Function to the Base Class

    通过变量.BaseClass.function的方式执行base class的function。

  • Call Overridden Function From Derived Class

    在Derived Class中调用Base Class的同名方法

  • Call Overridden Function Using Pointer

    通过父指针指向一个Derived Class的instance,来调用function,会调用到的Base Class的function。

    按照我的理解,C++读取instance需要按照某种Class来读取,当使用了Base Class的指针来调用instance的function,则会按照Base Class中的函数名对应的具体逻辑代码来执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // C++ program to access overridden function using pointer
    // of Base type that points to an object of Derived class

    #include <iostream>
    using namespace std;

    class Base {
    public:
    void print() {
    cout << "Base Function" << endl;
    }
    };

    class Derived : public Base {
    public:
    void print() {
    cout << "Derived Function" << endl;
    }
    };

    int main() {
    Derived derived1;
    // pointer of Base type that points to derived1
    Base* ptr = &derived1;
    // call function of Base class using ptr
    ptr->print();
    return 0;
    }

Output: Base Function

模板

类模板

specialization members和partial specializations参考原文参考

specialization members

对某写特定类型定制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

template <typename T>
struct myclass {
void print() { std::cout << "myclass!" << std::endl; }
};

//定制成员的int版本
template <>
void myclass<int>::print() {
std::cout << "myclass! int" << std::endl;
}

int main(void) {
myclass<double> mcd;
mcd.print();

myclass<int> mci;
mci.print();

return 0;
}

partial specializations

对类型的不同形式(指针,引用...)定制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

template <typename T>
struct myclass {
void print() { std::cout << "myclass!" << std::endl; }
};

// 类的部分定制, 左值
template <typename T>
struct myclass<T&> {
void print() { std::cout << "myclass! lvalue" << std::endl; }
};

// 右值
template <typename T>
struct myclass<T&&> {
void print() { std::cout << "myclass! rvalue" << std::endl; }
};

int main(void) {
int i(1988);
int& ri = i;
myclass<decltype(1988)> mc; // 原始版本
mc.print();

myclass<decltype(ri)> mcl; // 左值版本
mcl.print();

myclass<decltype(std::move(i))> mcr; // 右值版本
mcr.print();

return 0;
}

Traits

通过模板进行类型萃取(STL中的重要特性)如下代码链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>

/*
version 1
只能进行参数推导,不能进行返回值推导
*/
template <class I, class T>
void func_impl(I iter, T t) {
T tmp = *iter;
}

template <class I>
inline void func_0(I iter) {
func_impl(iter, *iter);
}

/*
version 2
在迭代器中指定类型, 可以指定返回值的类型,但是无法处理原生指针的情况
*/
template <class T>
struct MyIter {
typedef T value_type;
T* ptr;
MyIter(T* p = 0) : ptr(p) {}
T& operator*() const { return *ptr; }
};

// 在返回值中可以声明类型,但不能萃取原生指针
template <class I>
typename I::value_type func_1(I ite) {
return *ite;
}

/*
version 3
在version2的基础之上,添加partial specialization
*/
template <class I>
struct iterator_traits {
typedef typename I::value_tpye value_type;
};

// 偏特化,指针类型的会使用这个版本
template <class I>
struct iterator_traits<I*> {
typedef I value_type;
};

// 在返回值中可以声明类型
template <class I>
typename iterator_traits<I>::value_type func_2(I ite) {
return *ite;
}

int main() {
int* a = new int(2);
MyIter<int> b(a);

// std::cout << func_1(a) << std::endl; // error 无法萃取
std::cout << func_1(b) << std::endl; // 可以萃取
// 偏特化之后,两种指针都可以了
std::cout << func_2(a) << std::endl;
std::cout << func_2(a) << std::endl;
}

模板类

在编译时期产生,会生成具体的类,可以理解是类模板实例化后的一个产物

访问修饰符

Cpp中的访问修饰符有publicprivateprotected

修饰符 访问
private 可以在class内或者friend classes and functions访问
protected 在private的基础上,还可以被派生的类访问
public 可以被所有的class和function访问

Note: class中的成员,默认是private的;在struct中的默认是public。

编译执行

预处理 -> 编译 -> 汇编 -> 链接

预处理

写好的⾼级语⾔的程序⽂本⽐如 hello.c,预处理器根据#开头的命令,修改原始的程序,如#include<iostream> 将把系统中的头⽂件插⼊到程序⽂本中,通常是以.i 结尾的⽂件。

编译阶段

编译器将 hello.i ⽂件翻译成⽂本⽂件 hello.s,这个是汇编语⾔程序。⾼级语⾔是源程序。所以注意概念之间的区别。汇编语⾔程序是⼲嘛的?每条语句都以标准的⽂本格式确切描述⼀条低级机器语⾔指令。不同的⾼级语⾔翻译的汇编语⾔相同。编译策略可以分为静态编译和动态编译:

静态编译

编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,连接到可执⾏⽂件中去,使可执⾏⽂件在运⾏时不需要依赖于动态链接库;

动态编译

可执⾏⽂件需要附带⼀个动态链接库,在执⾏时,需要调⽤其对应动态链接库的命令。所以其优点⼀⽅⾯是缩⼩了执⾏⽂件本身的体积,另⼀⽅⾯是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只⽤到了链接库的⼀两条命令,也需要附带⼀个相对庞⼤的链接库;⼆是如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不能运⾏。

汇编阶段

汇编器将 hello.s 翻译成机器语⾔指令。把这些指令打包成可定位⽬标程序,即 .o⽂件。hello.o是⼀个⼆进制⽂件,它的字节码是机器语⾔指令,不再是字符。前⾯两个阶段都还有字符。

链接阶段

⽐如 hello 程序调⽤ printf 程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于⼀个名叫 printf.o 的单独编译好的⽬标⽂件中,这个⽂件将以某种⽅式合并到 hello.o 中。链接器就负责这种合并。得 到的是可执⾏⽬标⽂件。链接也可以分为动态链接和静态链接。下面简单介绍,更多介绍可参考《操作系统-精髓于设计原理》读书笔记中的附录 7A 加载和链接

静态链接

静态连接库就是把 (lib) ⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件;

动态链接

动态链接就是把调⽤的函数所在⽂件模块(DLL)和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候 再从 DLL 中寻找相应函数代码,因此需要相应 DLL ⽂件的⽀持。

Reference