C++ 杂记

本文最后更新于:2024年12月22日 凌晨

00

基础

内存分区

  1. 代码区:函数体的二进制代码
  2. 全局区:全局变量、静态变量、常量
  3. 栈区:函数调用、局部变量,编译器自动分配
  4. 堆区:程序员动态分配内存,如new和delete

枚举

enum enum_name{a,b,c..} var_name;
枚举标识符可赋值,默认从0开始+1

const

常类型的变量或对象的值无法被更新

修饰变量

必须赋初始值,且赋值之后无法修改

与#define相比,const定义的常量有数据类型,编译器可以进行类型安全检查,而#define只是单纯的字符串替换

可以防止修改,提高代码健壮性;同时节省空间

const变量默认为文件作用域,如果想在其他文件中使用,需要加extern

修饰指针

int const *p;const int *p;: const在*左边,无法通过指针修改这个变量的值指向常量的指针

无法通过这个指针修改,但仍然可以通过变量名、引用,或者其他指针修改

int * const p: const在*右边,指针指向的地址不能被修改 常指针

定义时必须初始化,常指针无法修改指向的地址,即使两个变量指向同一块地址

修饰引用

同理,不能通过引用修改值,但仍可以通过变量名修改

修饰函数返回类型

函数返回值时,加const无意义,因为本身返回的值也会赋给其他变量,该值就可以通过其他变量修改

返回指针时同上

修饰函数形参同上

修饰类、对象、成员函数

const对象只能访问const成员函数,const成员函数可以访问所有成员变量和其他const成员函数,但无法修改

static

为什么引入static:函数内部定义的变量,程序执行到时才分配内存到栈区,函数运行结束即释放。但有时候想要保存该变量的值到下一次调用,同时又不想改变该变量的访问范围

修饰成员变量

在程序启动时就被创建,不依赖类的对象,可以使用类名来直接调用

修饰成员函数

同样不依赖于类的对象,无法使用this指针,只能访问静态成员变量/函数

struct和class

区别:struct的默认访问和继承权限是public,而class的默认访问和继承权限是private

define和typedef

define只做单纯字符串替换,没有类型检查,在预处理阶段起作用;typedef相当于类型别名,在编译运行时起作用

new和malloc

new是一个操作符,使用时会初始化一个对象,调用构造函数,返回一个指向新分配空间的对象实例的指针,因此无需指定内存大小,使用完毕指针销毁时并不会自动回收内存,因此需要手动delete来调用对象的析构函数释放

malloc是一个库函数,使用时仅仅显式的在堆上分配指定大小的内存

面向对象

构造函数

编译器默认为每个对象提供空的构造函数和析构函数,以提供对象初始化和清理功能,也可以自定义

class_name(){} 可以有参数,可以重载,程序调用对象前自动执行

编译器默认会给一个类添加三个函数:默认构造函数、拷贝构造函数、析构函数

如果定义了有参构造,编译器不再提供默认构造,需要手动定义;同理,如果要自定义拷贝构造函数,编译器也不会再自动生成其他构造函数

析构函数

~class_name(){} 不能有参数,无返回值,程序结束时自动调用

深拷贝和浅拷贝

浅拷贝:只是简单的拷贝,指针指向的地址相同,两个对象指向同一块内存,释放一个对象的内存会导致另一个对象的内存无效

深拷贝:重新在堆区分配一块内存,将原对象的值拷贝到新的内存中,两个对象指向不同的内存

对于拷贝构造来说,如果类中有属性是在堆区开辟的(比如说被拷贝的对象中有指针,指针指向堆区new出来的一块内存),那么在拷贝时也需要重新在堆区开辟内存,并将原对象中的值拷贝到新的内存中

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

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

Person(const Person& p) {
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}

~Person() {
if (m_height != NULL){delete m_height;}
}
public:
int m_age;
int* m_height;
};

如上,m_height本身是指针,构造时在堆区创建一块内存并指向该内存。如果在拷贝时仅仅做m_height = p.m_height,会导致两个对象指向同一块内存,释放一个对象的内存会导致另一个对象的内存无效

构造与析构顺序

继承

基类构造->派生类构造->派生类析构->基类析构

成员对象

成员对象构造->外层对象构造->外层对象析构->成员对象析构

this指针

类的每个成员函数只会诞生一份函数实例放入代码区,多个同类型的对象会共用这份代码
在调用成员函数时,C++内置this指针,用于指向调用该函数的对象

友元friend

修饰全局函数

将全局函数在类中声明为友元,就可以访问该类的私有成员

修饰类

将类A在类B中声明为友元,A就可以访问B的私有成员

修饰成员函数

将类A的成员函数在类B中声明为友元,A的成员函数就可以访问B的私有成员

继承

无论怎么继承,都无法访问从父类继承的私有成员。
私有成员还是会继承,但只是被隐藏了

公共继承 class A : public B

父类中的公共和保护成员类型不变

保护继承 class A : protected B

父类中的公共和保护成员变为保护成员

私有继承 class A : private B

父类中的公共和保护成员变为私有成员

多态

如果有多个派生类且需要给他们分别实现一个同名的行为,比如说一个动物类,有狗、猫、猪等派生类,都有一个叫的行为,但是叫的实现不同,这时候就可以使用多态

多态的必须条件:父类指针指向子类对象,其实就是该指针将这个子类对象当作一个其父类,因此可以调用父类中的虚函数,同时由于指向的是子类对象,所以调用的是子类中重写的函数

虚函数 virtual

在基类中,将需要多态的函数以virtual关键字声明,并在派生类中重写该函数。此后,若是父类指针指向子类对象时,并使用该指针调用多态函数,就会调用子类的函数

virtual的意思类似“这个函数可能会被子类重写”,告诉编译器先不急着确定函数地址,编译器会根据指针指向的对象来动态调用对应的函数

纯虚函数

在需要使用多态的场景中,往往基类中的虚函数只是为了让派生类重写,并无实际意义,此时可将虚函数改为纯虚函数

void func()
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

有纯虚函数意味着这个类仅仅用来声明派生类的接口,称为抽象类,无法被实例化

#### 虚析构
在父类指针指向子类对象时,如果释放该指针,只会调用父类的析构函数。这是因为在编译时,编译器只知道指针的
类型是父类,而不知道指针指向的是子类对象。

因此,如果想要调用子类的析构函数,需要将父类的析构函数声明为虚析构```virtual ~类名(){}```

同理纯虚析构```virtual ~类名() = 0``` 和纯虚函数一样,纯虚析构意味着该类是抽象类,除此之外和虚析构没啥区别

### 构造/析构能否设置为虚函数
构造函数不能:虚函数的原意是只知道接口不用知道对象完整信息的情况下完成某个工作,构造函数在创建对象时显然需要对象的完整信息,因此不应该定义成虚函数

析构函数需要:在派生类对象中有额外的内存在析构时需要回收时,如果析构函数不是虚函数,就不会触发动态绑定,派生类析构只会调用基类析构,造成内存泄漏

### 智能指针
智能指针是一种用于自动管理内存的工具,在原先的new分配内存时,由于指针并没有自动回收内存的功能,会存在因为忘记delete或者多次delete造成内存泄漏的隐患

而智能指针是一个模板类,在他的生命周期结束时会调用本身的析构函数,自动进行空间回收

std::unique_ptr:独占管理分配的对象,确保只有一个指针拥有指定的内存资源
std::shared_ptr:共享指针,内部会使用一个引用计数器来跟踪对象被共享的次数,只有在计数为0的时候才释放内存
std::weak_ptr:弱引用指针,不增加引用计数器,必须配合共享指针使用

### 模板 template<class/typename T>
在下面的函数或者类中定义一种通用数据类型,在调用时可以指定具体的数据类型,提高代码复用

### STL:标准模板库
三大组件:容器、算法、迭代器
容器和算法之间通过迭代器无缝衔接

#### 列表
```C++
vector<int> nums = {1, 2, 3, 4, 5};
nums.push_back(6); //在末尾添加元素
nums.pop_back(); //删除末尾元素
nums.insert(nums.begin() + 1, 10); //在指定位置插入元素
nums.erase(nums.begin() + 1); //删除指定位置元素
nums.clear(); //清空

数组

1
2
3
4
5
array<int, 5> arr = {1, 2, 3, 4, 5};
arr.at(1); //获取指定位置元素
arr.front(); //获取首元素
arr.back(); //获取尾元素
arr.fill(0); //填充

1
2
3
4
5
6
stack<int> s;
s.push(1); //入栈
s.pop(); //出栈
s.top(); //获取栈顶元素
s.empty(); //判断是否为空
s.size(); //获取栈大小

队列

1
2
3
4
5
6
7
queue<int> q;
q.push(1); //入队
q.pop(); //出队
q.front(); //获取队首元素
q.back(); //获取队尾元素
q.empty(); //判断是否为空
q.size(); //获取队列大小

双向队列

1
2
3
4
5
6
7
8
9
deque<int> dq;
dq.push_back(1); //尾部入队
dq.pop_back(); //尾部出队
dq.push_front(2); //头部入队
dq.pop_front(); //头部出队
dq.front(); //获取队首元素
dq.back(); //获取队尾元素
dq.empty(); //判断是否为空
dq.size(); //获取队列大小

哈希表

1
2
3
4
5
6
7
8
unordered_map<int, string> m;
m[1] = "one"; //插入
m[2] = "two";
m[3] = "three";
m.erase(2); //删除
m.find(1); //查找
m.size(); //获取大小
m.empty(); //判断是否为空

C++ 杂记
http://example.com/2024/03/11/C-杂记/
作者
Zhang Yix
发布于
2024年3月11日
更新于
2024年12月22日
许可协议