0%

重学C++学习笔记(二)

上一篇,本篇主要是面向对象相关的知识点

C++可以使用structclass定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 默认为public权限
struct Person {
int age;
void run {
cout << "person run" << endl;
}
};

// 默认为private,需要显示声明public
class Person {
public:
int age;
void run {
cout << "person run" << endl;
}
}

structclass的区别: class默认权限为private,struct默认权限为public,推荐使用class

嵌套类

C++中类支持嵌套定义,本质是语法糖,用于限制作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
class Car {

}
}

void func() {
// 局部类,可以在函数内使用
class Point {
int x;
int y;
}
}

函数调用

1
2
3
Person p;
p.age = 20;
p.run();

转成汇编

1
2
3
4
5
6
7
mov d word ptr [ebp-0Ch], 0Ah

; 传递调用者到exc,也就是p
lea ecx, [ebp-0Ch]

; 调用函数
call 00061366

C++对象的内存结构如下

对象的本质,其实就是字段(和虚表指针)的内存集合,先定义的对象会放在低地址,后定义的对象防止高地址

this

this是调用对象的地址,为指针,当对象调用成员函数的时候,会自动传入当前对象的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
private:
int m_age;
public:
void func() {
this -> m_age = 0;
// 编译成汇编
// 0x10c9c2f7c <+12>: movl $0x0, (%rax) ; 取到寄存器rdx存放的地址,给该地址赋值0
}
};

int main() {
Person p = Person();
p.func();
return 0;

// 生成汇编(AT&T)
// 0x10c9c2f50 <+32>: movq %rax, -0x10(%rbp) ; 设置p变量
// 0x10c9c2f54 <+36>: callq 0x10c9c2f8a ; 初始化Person的内存
// 0x10c9c2f59 <+41>: movq -0x10(%rbp), %rdi ; 将p的地址赋值给寄存器rdi
// 0x10c9c2f5d <+45>: callq 0x10c9c2f84 ; 调用函数

}

由汇编可以看出,通过寄存器传递this指针,this指针存放对象的地址

堆空间初始化

1
2
3
4
5
6
7
8
9
10
int *p1 = new int;          // 不会被初始化
int *p2 = new int(); // 初始化为0
int *p3 = new int(5); // 初始化为5
int *p4 = new int[3]; // 数组未被初始化
int *p5 = new int[3](); // 数组元素都被初始化为0
int *p6 = new int[3]{}; // 数组元素都被初始化为0
int *p7 = new int[3] {5}; // 数组首元素被初始化为5,其他被初始化为0

Person *p8 = new Person; // 成员变量不会被初始化
Person *p9 = new Person(); // 成员变量默认被初始化为0,或调用构造函数

对象内存分布

1
2
3
4
5
6
7
8
9
10
11
// 全局变量,全局区
Person g_person;

int main() {
// 局部变量,栈空间
Person person;

// 手动申请内存,堆空间
Person *p = new Person();
return 0;
}

构造函数/析构函数

构造函数支持重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Person {
int m_age;
Person() {
m_age = 10;
}

// 重载构造函数
Person(int age) {
m_age = age;
}

// 析构函数,只有一个
~Person() {
// 对象释放之前调用
}
};

Person person(20);
cout << person.age << endl;

如果自定义了构造函数,除了全局区(默认会初始化为0),其他内存空间的成员变量都需要手动初始化

1
2
3
4
5
6
7
8
9
struct Person {
int age;
Person() {
// age的值不确定,需要手动初始化age变量

// 所有成员初始化为0
msmset(this, 0, sizeof(Person));
}
};

构造函数和析构函数必须是public,如果定义为private,则该类无法被外部创建
构造函数先调用父类,后调用子类
析构函数先调用子类,后调用父类

成员访问权限

权限与JAVA一样,对类的成员变量声明权限(struct默认为public,class默认为private)

  • public:公开访问(struct默认
  • protected:当前类和子类访问
  • private:私有(class默认

继承关系也可以添加权限,表示继承的成员对子类的权限,struct默认为public,class默认为private,class要手写public

1
2
3
4
// Student的子类不能访问Person的成员,叠加权限使用最小权限
struct Student: private Person {
int m_score;
};

初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person {
int m_age;
int m_height;

// Person(int age, int height) {
// m_age = age;
// m_height = height;
// }

// 与上面构造方法等价
Person(int age, int height): m_age(age), m_height(height) {

}
};

初始化顺序跟成员变量的声明顺序有关系,和列表顺序无关,下面写法是一样的

1
2
3
4
Person(int age, int height): m_age(age), m_height(height) { }

// 与上面等价,先初始化m_age,再初始化m_height
Person(int age, int height): m_height(height), m_age(age) { }

构造函数互相调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person {
int m_age;
int m_height;

// 必须写在后面
Person() : Person(0, 0) {
// 这里相当于创建一个新的对象
// Person(0, 0);
}

Person(int age, int height) {
m_age = age;
m_height = height;
}
};

父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Person {
private:
int m_age;
Person() {
m_age = 0;
}

Person(int age): m_age(age) {

}
};

struct Student : Person {
// 默认是调用无参构造函数
// Student() : Person() {

// 显示调用父类构造函数
Student() : Person(10) {

}
};
  • 如果父类没有构造函数,则不调用
  • 如果父类有构造函数,子类没有定义构造函数,则子类会隐式生成构造函数,并且调用父类的构造函数

调用父类方法

继承关系中调用父类方法,通过类名调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
public:
void func() { }
}

class Student: public Person {
public:
void func() {
// 调用父类方法,相当于swift的super.func()
Person::func();
}

//运算符重载
Student &operator=(const Student &student) {
// 调用父类的方法
Person::operator=(student);
...
}
}

拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
public:
int m_age;

// 默认无参构造函数
Person() {}

// 拷贝构造函数,赋值操作会调用拷贝构造函数
Person(const Person &person): m_age(person.m_age) { }
};

int main() {
// p1, p2通过无参构造函数构造
Person p1 = Person();
Person p2;

// p3, p4通过拷贝构造函数构造
Person p3 = p1;
Person p4(p1);

// 非初始化赋值,直接做变量内存拷贝,不会调用构造函数
p2 = p1;
return 0;
}

隐式构造

1
2
3
4
5
6
7
8
9
10
11
class Person {
private:
int m_age;
public:
Person(int age): m_age(age) { }
};

Person p1(10);

// 隐式构造,相当于:Person p2 = Person(20)
Person p2 = 20;

可读性差,尽量不用,可以通过关键字explicit禁用隐式构造

1
2
// 不能通过 Person p2 = 20; 构造
explicit Person(int age): m_age(age) { }

自动生成构造函数

在下面情况下,编译器会自动为类生成无参构造函数

  • 父类存在构造函数,子类没有定义构造函数
  • 虚继承(需要做虚表指针的初始化)
  • 有虚函数(需要做虚表指针的初始化)
  • 类包含了有构造函数的成员,并且没有定义构造函数,则编译器会自动生成构造函数初始化成员
  • 类的字段在声明的时候进行了初始化

仿函数

相当于直接调用对象,本质是重载()运算符

1
2
3
4
5
6
7
8
9
10
class Sum {
public:
int operator()(int a, int b) {
return a + b;
}
}

auto sum = Sum();
// res = 30
auto res = sum(10, 20);

深拷贝/浅拷贝

  • 浅拷贝: 编译器默认提供的拷贝为浅拷贝,将一个对象所有成员变量的值拷贝到另一个对象(指针变量只拷贝地址,不会拷贝指针指向的内存空间),浅拷贝会带来一个问题,就是多次free的问题

    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
    class Car {
    public:
    Car() {}
    };

    class Person {
    private:
    int m_age;
    Car *car;
    public:
    Person() {
    car = new Car();
    }
    ~Person() {
    if (car != nullptr) {
    delete car;
    car = nullptr;
    }
    }
    };

    int main() {
    {
    // 下面代码会导致Person的析构函数调用了两次
    Person p1 = Person();
    Person p2 = p1;
    }
    }
  • 深拷贝:需要自定义拷贝构造函数实现,手动将指针类型的变量指向的内容拷贝一份新的

多态

C++的多态通过虚函数来实现

虚函数

需要在父类方法实现virtual函数才能使用多态,否则不是多态(不加virtual,编译器直接根据类型调用对应的函数)

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
struct Animal {
void speak() {
cout << "animal speak" << endl;
}
};

struct Dog: Animal {
// 子类不用声明virtual,默认就是虚函数
void speak() {
cout << "dog speak" << endl;
}
};

void func(Animal *ani) {
// 由于speak是普通函数,编译之后,会直接调用Animal::speak
ani.speak();
// call [0A3C] ; Animal::speak
}

int main() {
Dog *dog = new Dog();
func(dog);
// 输出 animal speak,而我们希望输出 dog speak

delete dog;
return 0;
}

修改Animal::speak改为virtual

1
2
3
4
5
6
7
struct Animal {
virtual void speak() {
cout << "animal speak" << endl;
}
};

// 输出 `dog speak`
  • 纯虚函数:没有实现的虚函数,后面用等于0
  • 抽象类:含有纯虚函数的类,抽象类不能被实例化
1
2
3
4
5
struct Animal {
// 纯虚函数,等于0是固定写法
virtual void speak() = 0;
virtual void run() = 0;
}

虚表

虚函数的多态特性是通过虚表来实现的,如果一个类对象有虚函数,则会多4或8个字节,32或64位环境不同,并且多出来的内存是放在对象首地址

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Animal {
int m_age;
virtual void speak() {
cout << "animal speak" << endl;
}

virtual void run() {
cout << "animal run" << endl;
}
};

cout << sizeof(Animal) << endl;
// 输出8 (4 + 4)(x86环境)

原理:多出的4/8个字节用来存储虚函数表的地址,虚函数表存放着对象的虚函数地址,编译器在运行时通过该表,找到对应的函数地址执行,从而达到多态的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animal *ani = new Dog();
// speak是虚函数
ani.speak();

// 编译为汇编
// mov eax, dword ptr [epb-8] ; [epb-8]是ani指向的地址,即Dog的地址
// mov edx, dword ptr [eax] ; 取出eax头4个自己的值(即虚表地址),存放到edx
// mov eax, dword ptr [edx] ; 取出edx的前4个字节(即虚表中的第一个函数地址),存放到eax
// call eax ; 调用寄存器eax指向的函数地址

ani.run();

// 转成汇编和speak方法是一样的,在从虚表取函数地址的时候,会加上对应的偏移量
// mov eax, dword ptr [edx+4] ; 从edx地五个字节开始读(即虚表中的第二个函数地址),存放到eax

从汇编代码可以看出,虚函数的调用过程

  1. 通过对象存放的虚表地址(对象首地址)
  2. 通过虚表地址找到虚表,从虚表中找到对应函数的地址
  3. 调用函数

同个类所有对象共用一份虚表,不管对象使用什么指针接收,最终都会调用虚表的方法,也就是对象真正的方法

虚方法

注意:如果子类没有重写父类的虚函数,父类的虚函数编译的时候也会被放到子类的虚表里面,也就是函数调用在编译的时候就确定了,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Animal {
virtual void speak() { }
virtual void run() { }
}

struct Dog : Animal {
void run() { }
}

struct WhiteDog: Dog {

}

WhiteDog *dog = new WhiteDog();

// dog对象的虚表存放了 Dog::run 和 Animal::speak 方法的地址

多态的调用行为在编译后就确定了,而不是运行时动态确定

多态-析构函数

由于C++多态使用的虚表实现的,对于多态,析构函数也要使用虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;

struct Animal {
// 父类的析构函数
~Animal {
cout << "Animal::~Animal" << endl;
}
}

struct Dog: Animal {
// 父类的析构函数
~Dog {
cout << "Dog::~Dog" << endl;
}
}

Animal *ani = new Dog();
// 这里值调用Animal的析构函数,不会调用Dog的析构函数
delete ani;

需要把Animal的析构函数设置为virtual

1
2
3
4
5
6
struct Animal {
// 父类的析构函数
virtual ~Animal {
cout << "Animal::~Animal" << endl;
}
}

多态继承

如果要用父类指针指向子类对象,则继承的权限必须是public

1
2
3
4
5
6
7
8
9
class Person { }

// 必须public继承Person
class Student: public Person {

}

// 多态
Person *person = new Student()

调用父类方法

直接通过类名调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;

struct Animal {
virtual void speak() {
cout << "animal speak" << endl;
}

void run() { }
}

struct Dog: Animal {
void speak() {
// 直接调用父类的方法
Animal::speak();

// 调用父类的成员函数
Animal::run();

cout << "animal speak" << endl;
}
}

多继承

C++支持多继承(不建议使用,会增加程序设计的复杂度)

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 Student {
public:
int m_score;
void study() { }
};

class Worker {
public:
int m_salary;
void work() { }
};

class Undergraduate: public Student, public Worker {
public:
int m_grade;
void play() { }
};

int main() {
Undergraduate ug;
ug.m_score = 10;
ug.m_salary = 20;
ug.m_grade = 30;
ug.study();
ug.work();
ug.play();

return 0;
}

内存分布

多继承-虚函数

多继承的多态也是通过虚函数实现的,单继承一样,但是由于又多个父类,多继承会又多个虚表地址

多继承的子类会产生多张虚表,分别对应不同父类

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
class Student {
public:
int m_score = 1;
virtual void func() {
cout << "student func" << endl;
}
};

class Worker {
public:
int m_salary = 2;
virtual void func() {
cout << "worker func" << endl;
}
};

class Undergraduate : public virtual Student, public virtual Worker {
public:
int m_grade = 3;
void func() {
cout << "undergraduate func" << endl;
}
};

int main() {
Undergraduate *undergraduate = new Undergraduate();
Student *stu = undergraduate;
cout << stu->m_score; << endl;
stu->func();
return 0;
}

undergraduate对象内存分布

同名函数/变量问题

对于同名变量,C++默认会当成多个变量来存储

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
class Student {
public:
int m_age = 1;
}
class Worker {
public:
int m_age = 2;
}
class Undergraduate {
public:
int m_age = 3;
}

int main() {
Undergraduate *under = new Undergraduate();
// 默认访问 Undergraduate::m_age == 3
cout << under->m_age << endl;

// 多态,这里不支持,需要通过虚继承实现
Student* stu = under;

// 访问的是Student::m_age == 1
cout << stu->m_age << endl;
return 0;
}

内存分布

菱形继承

由于C++支持多继承,所以会出现菱形继承这种情况,我们知道子类会继承父类的所有成员变量(包括同名的)

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
class Person {
public:
int m_age = 1;
};

class Student: public Person {
public:
int m_score = 2;
};

class Worker: public Person {
public:
int m_salary = 3;
};

class Undergraduate : public Student, public Worker {
public:
int m_grade = 4;
};

int main() {
Undergraduate *under = new Undergraduate();

// 不能直接引用,存在二义性,下面代码报错
under->m_age = 10;

// 可以通过指定类类访问
under->Student::m_age = 11;
under->Worker::m_age = 11;

return 0;
}

Undergraduate的内存分布

虚继承

上面Undergraduate会继承两份Person的成员m_age,为了避免这种情况(通常我们希望只继承一份成员变量),C++使用虚继承解决

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 Person {
public:
int m_age = 1;
};

// 虚继承基类
class Student: virtual public Person {
public:
int m_score = 2;
};

// 虚继承基类
class Worker: virtual public Person {
public:
int m_salary = 3;
};

class Undergraduate : public Student, public Worker {
public:
int m_grade = 4;
};

int main() {
Undergraduate *under = new Undergraduate();

under->m_age = 10;
cout << sizeof(Undergraduate) << endl;
return 0;
}

内存分布

可以看到出多了两个虚表,用于声明基类字段m_age的偏移量,m_age只有一份

静态成员

  • 静态成员存储在数据段(全局区)整个程序运行过程中只有一份内存

  • C++的静态成员必须初始化,并且必须在类外面初始化

    1
    2
    3
    4
    5
    6
    7
    class Student {
    // 声明
    static int m_count;
    };

    // 初始化
    int Student::m_count = 10;

友元

友元方法:可以在类外部的方法访问类的所有成员(字段和方法)
友元类:可以在其他类访问类的所有成员(字段和方法)

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
class Point {
// 声明友元方法
friend Point add(const Point &a, const Point &b);

// 声明友元类
friend class Math;

private:
int m_x;
int m_y;
// 私有方法
void func() { }
public:
Point(int x, int y): m_x(x), m_y(y) {
}
};

Point add(const Point &a, const Point &b) {
// 在这里可以直接访问Point的私有变量
return Point(a.m_x + b.m_x, a.m_y + b.m_y);
}

class Math {
void func() {
Point p = Point(1, 2);
// 可以访问类的私有成员
p.m_x = 10;
p.m_y = 20;
// 访问私有方法
p.func();
}
};

下一篇主要是C++的新特性