学习Android源码时,需要一定的C++基础,因此就有了这份C++学习笔记。
我是找的视频学习的,先贴下视频链接:
- 油管
- b站:
- 中文字幕:www.bilibili.com/video/BV1uy…
- 中文字幕 + Ai中文配音:www.bilibili.com/video/BV1UH…
1.Hello World!(C++)
按照惯例,先来个Hello World!
运行代码会输出Hello World!到控制台。 return 0;这行是可以省略的,C++中的mian函数是一个特例,如果没有任何返回,系统将视为返回0
2.预处理语句
2.1.#include
在C++中,通常的代码组织方式是:
-
头文件(.h/.hpp) :
- 包含函数声明(也称为函数原型)
- 包含类定义
- 包含宏定义
- 包含类型定义
-
源文件(.cpp) :
- 包含函数的具体实现(定义)
- 包含变量的定义
也就是说
- 在头文件中进行 声明 (declaration)
- 在源文件中进行 定义 (definition)
#include "xxx.h"的作用是指定要包含的xxx.h文件,预处理器将打开该文件,读取全部内容,复制粘贴到当前写了include的文件
新建头文件Log.h
void log(char* message);
新建对应的源文件Log.cpp
#include<iostream>
void log(char* message) {
std::cout << message << std::endl;
}
在Main.cpp中调用log函数
#include "Log.h"
int main() {
char msg[] = "Hello, World!";
log(msg);
}
执行main函数,正常打印了Hello World! 打开预处理C++代码Main.i文件:
void log(char* message);
int main() {
char msg[] = "Hello, World!";
log(msg);
}
可以看到#include "Log.h"就是将Log.h文件中的所有代码全部复制粘贴到了Main.cpp中
举个Android源码中的栗子。
在Android系统启动流程中的System/core/init/Main.cpp文件
执行main函数,最开始会跑到FirstStageMain中,在first_stage_init.h中声明了这个函数
在first_stage_init.cpp中定义了这个函数
2.2.#define A B
define预处理语句会搜索A, 然后使用B替换A
#define INTERGER double
INTERGER Multiply(INTERGER a, INTERGER b) {
return a * b;
}
打开预处理的C++文件,即xxx.i。代码如下
double Multiply(double a, double b) {
return a * b;
}
2.3.#if
if预处理语句:根据给定的条件包含或移除代码
2.3.1 if 0
#include "Log.h"
#if 0
int main() {
char msg[] = "Hello, World!";
log(msg);
}
#endif
对应的预处理C++代码:
void log(char* message);
2.3.2 if 1
修改Main.cpp
#include "Log.h"
#if 1
int main() {
char msg[] = "Hello, World!";
log(msg);
}
对应的预处理C++代码:
void log(char* message);
int main() {
char msg[] = "Hello, World!";
log(msg);
}
在C++中,0代表false,任何非零值(包括正数、负数)都为 true。
2.4.其他
比如: #pragma once // 防止头文件重复包含
这些就不一一介绍了,感兴趣可以自己找下相关资料
3.指针
定义:指针是一个地址,一个保存内存地址的整数
注意:所有类型的指针都是保存内存地址的整数
举个栗子
当想知道一个变量在内存中的地址,可以使用&运算符,就跟上面代码中一样。那我们来看看prt这个指针的值是多少
可以清楚的看到: prt指针的地址是0x000000E2A854FAC0, 这个内存地中存的值是5。
不用将指针想的太多麻烦,指针就是一个内存地址整数
那么指针的指针又是什么呢?
再来个栗子
prt的值是0x00000065D44FF910,而poi是指向prt这个指针的指针,它的值是10 f9 4f d4 65 00 00 00。指针的指针就是指针的地址,这里的值怎么是相反的?是因为我用的电脑是Windows。
- Windows系统使用的是x86/x64架构,采用小端序
- 小端序的特点是:最低有效字节(LSB)存储在最低的内存地址
- 即数值的"低位字节"放在内存的"低地址"
内存地址增加方向 →
10 f9 4f d4 65 00 00 00
↑ ↑
LSB(最低位) MSB(最高位)
转换方法 :
- 每2个十六进制数字是一个字节(byte)
- 从右往左读取字节:
00 00 00 65 d4 4f f9 10 → 0x00000065D44FF910
4.引用
定义:引用只是指针的伪装,只是指针的语法糖
注意:引用不是新的变量,不占用内存
4.1.举个栗子
上面代码会输出Value of a: 10
在这里,ref就是a的一个别名,修改ref,也就是修改a
4.2.再来个栗子
上面代码会输出Value of a: 5
在Increment函数中的参数是值传递。调用函数时,实参 a 的值会被复制一份给形参 value, 函数内修改的只是这个副本,不影响原始变量a。
4.3.栗子3 使用指针
上面代码会输出Value of a: 6
执行过程:
1.调用前:假定a的内存地址是0x1000,a 在内存地址0x1000处存储值5
2.调用时:将 &a (0x1000)赋值给指针;
3.(*value)++:
- *value 找到地址0x1000处的值5;
- 执行 (*value)++ 将该内存值改为6;
4.函数返回:指针 value 被销毁,但已修改原始内存
4.4.栗子4 使用引用
这同样会输出Value of a: 6
现在再看看这句话:引用只是指针的伪装,只是指针的语法糖。
比如下面使用引用和指针都能修改变量的值
使用指针:
使用引用:
需要注意的是:一旦声明了一个引用,就不能改变它引用的对象
这里会输出
Value of a: 8 Value of b: 8
首先会声明a的引用ref,然后赋值为了b,所以a,b的值都是8。
如果想先指向a,然后改为指向b
这里会输出
Value of a: 1
Value of b: 2
引用和指针的区别
-
引用必须初始化,不可改变绑定
-
指针可以改变绑定
5.类
定义:数据和处理数据的函数组合在一起
这里会输出
Value of player.x: 2
Value of player.y: 2
6.结构体
还是上面的代码,直接将class改为struct,并删除public
这里同样会输出
Value of player.x: 2
Value of player.y: 2
class与struct唯一区别:可见度
- class 默认是private
- struct 默认是public
使用习惯
- 结构体:只是数据的集合; 需要与C语言交互
- 类:复杂对象;需要被继承时
7.static
7.1.类和结构体之外的static
当声明静态函数或静态变量时,它只能在它被声明的C++文件中使用
在Static.cpp中定义变量a
int s_Value = 10;
Main.cpp也定义变量a
#include <iostream>
int s_Value = 20;
int main() {
std::cin.get();
}
运行报错: Static.obj : error LNK2005: "int s_Value" (?s_Value@@3HA) already defined in Main.obj
这是因为不能有两个同名的全局变量。
我们可以使用static修改这个错误,修改Static.cpp
static int s_Value = 10;
编译运行成功
加了static的s_Value只会在Static.cpp文件中可见,就不会有全局同名的两个变量了
当然还有一种方式也可以修改上面的报错,使用extern
#include <iostream>
extern int s_Value;
int main() {
std::cin.get();
}
注意这种方式需要在其他文件中定义s_Value变量,extern会去其他文件中寻找s_Value变量。如果找不到,也会报错。
继续说static
static用于函数时
在Static.cpp中定义函数
void function() {}
Main.cpp中同样定义这个函数
#include <iostream>
void function() {}
int main() {
std::cin.get();
}
运行报错:Static.obj : error LNK2005: "void __cdecl function(void)" (?function@@YAXXZ) already defined in Main.obj
原因跟上面的变量的例子是差不多的,这里定义了两个全局同名函数。
修改static.cpp或修改Main.cpp都行,添加static
static void function() {}
7.2.类和结构体中的static
在结构体中定义x,y,都是静态变量。
这里会输出两遍x: 5,y: 10
类和结构体中的static变量,在所有Entry实例中只会存在一份,也就是所有的Entry实例共享Entry结构体中的static变量。
需要再提一点的是
在结构体中static int x, y; 这里只是声明了x,y,告诉编译器存在x,y变量
实际的内存分配需要在类外完成,也就是int Entry::x; int Entry::y;
为什么必须在类外定义: 如果声明和定义都在类中,会造成重复定义,违反了单一定义规则。
在上面的e和e1中,因为e1修改了x,y。所以e中x,y也变成5和10。所以上面根据实例来给x,y赋值是没什么意义的,可以使用下面方式
那什么时候应该在类和结构体中定义静态变量?
- 想要在所有实例间共享数据
- 这个变量储存在类或结构体中是有意义,与类和结构体有关
再看看静态方法
#include <iostream>
struct Entry {
static int x, y;
static void print() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
};
int Entry::x;
int Entry::y;
int main() {
Entry::x = 1;
Entry::y = 2;
Entry::x = 5;
Entry::y = 10;
Entry::print();
Entry::print();
std::cin.get();
}
在结构体中定义x,y都是静态变量,print是静态方法。
这里同样会输出两遍x: 5,y: 10。
可以看到没有再去拿Entry实例。
但如果x,y不是静态的,而print是静态方法。
#include <iostream>
struct Entry {
int x, y;
static void print() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
};
int main() {
Entry e;
e.x = 1;
e.y = 2;
Entry e1;
e1.x = 5;
e1.y = 10;
Entry::print();
Entry::print();
std::cin.get();
}
print方法中会报错:E0245: a nonstatic member reference must be relative to a specific object
也就是静态方法无法访问非静态变量
这个原因是静态方法没有类或结构体的实例,非静态方法则会获取当前类的实例作为参数
如果我们需要在静态方法中访问非静态实例,需要通过参数传递实例
使用引用
struct Entry {
int x, y;
static void print(Entry& entry) {
std::cout << "x: " << entry.x << ", y: " << entry.y << std::endl;
}
};
// 调用方式
Entry e;
e.x = 1;
e.y = 2;
Entry::print(e);
使用指针
struct Entry {
int x, y;
static void print(Entry* entry) {
std::cout << "x: " << entry->x << ", y: " << entry->y << std::endl;
}
};
// 调用方式
Entry e;
e.x = 1;
e.y = 2;
Entry::print(&e);
这两种方式都可以。这里可以再想想上面说过的:引用是指针的伪装,是指针的语法糖
7.3.函数中的static
#include <iostream>
void add() {
int i = 0;
i++;
std::cout << i << std::endl;
}
int main() {
add();
add();
add();
std::cin.get();
}
这里会输出1 1 1 我们在变量前面添加static再看看
#include <iostream>
void add() {
static int i = 0;
i++;
std::cout << i << std::endl;
}
int main() {
add();
add();
add();
std::cin.get();
}
输出1 2 3.
函数中的static(static 对局部变量)的作用
- 生命周期延长:
- 普通局部变量:函数调用时创建,函数返回时销毁
- static局部变量:在程序启动时创建,程序结束时销毁
- 保持状态:static局部变量的值会在多次函数调用间保持
- 存储位置:从栈内存改为静态存储区
8.枚举
定义: 一个数值集合,一种命名值方法
enum Color {
Red,
Green,
Blue
};
就像上面这样使用enum就可以定义枚举类了。如果不给值,那默认从0开始,依次+1;如上面Red是0,Green是1,Blue是2
也可以自定义值是多少
enum Color {
Red = 2,
Green = 5,
Blue = 10
};
9.构造函数
构造函数通常用来设置变量或做任何需要的初始化工作
和Java的构造函数一样,C++的构造函数名也同样要和类名一致,也同样没有返回值。
但不同的是,C++中必须手动初始化所有基本类型,否则它们将设置为留在该内存中的其他值。Java中的基本数据类型,如int,会自动初始化为0。
#include <iostream>
class Car {
public:
int speed;
void print() {
std::cout << "Speed: " << speed << std::endl;
}
};
int main() {
Car c;
c.print();
std::cin.get();
}
C++的类有一个默认的构造函数。
在vs实际运行上面代码,会输出Speed:0。为什么不是随机数,可能与编译器有关,因为再定义一个成员变量,那么speed就会随机输出。
#include <iostream>
class Car {
public:
float speed;
Car(int spd) {
speed = spd
}
void print() {
std::cout << "Speed: " << speed << std::endl;
}
};
int main() {
Car c = Car(200);
c.print();
std::cin.get();
}
输出Speed:200
如果一个类,只有一些静态变量和静态方法,那么这个类没必要存在构造方法。
删除一个类的构造删除有两种方式:
1.使用private
class Car {
private:
Car() {}
public:
static void print() {
std::cout << "Car is running" << std::endl;
}
};
2.使用delete
class Car {
public:
Car() = delete;
static void print() {
std::cout << "Car is running" << std::endl;
}
};
C++构造函数需要注意的点
1.构造函数用不用new,会有一些区别
| 方式 | 不使用 new | 使用 new |
|---|---|---|
| 存储位置 | 栈(Stack) | 堆(Heap) |
| 分配时机 | 编译时确定(自动分配) | 运行时动态分配 |
| 内存管理 | 由编译器自动回收 | 需手动通过 delete 释放 |
| 生命周期 | 对象在作用域结束时销毁 | 对象生命周期由程序员控制 |
2.C++的构造函数还存在一些特殊的构造函数
- 复制构造函数
- 移动构造函数
- ...
10.析构函数
析构函数是用来卸载变量,清理使用过的内存
析构函数定义很简单,就是在构造函数前面加波浪号
#include <iostream>
class Car {
public:
Car() {
std::cout << "Car is create" << std::endl;
}
static void print() {
std::cout << "Car is running" << std::endl;
}
~Car() {
std::cout << "Car is destroyed" << std::endl;
}
};
void function() {
Car c;
c.print();
}
int main() {
function();
std::cin.get();
}
11.继承
Man类继承了Person类,那么父类中的方法和变量(非private时)就可以调用,如果子类对象没有重写父类方法则调用父类方法,重写了则调用自己的方法。
12.虚函数
前两个打印是正常的,我们通过创建对象,去调用各自的方法。但最后一个打印却跟预期是不同的,声明是Person类,但它的类型是Man类,应该调用Man类的方法,实际上调用的却是Person类。
原因:在声明函数时,方法通常在类内部。当调用方法时,会调用属于该类的方法
这就引出了本小节的虚函数。
虚函数引入了Dynamic Dispatch(动态联编),通常通过v表(虚函数表)来实现编译,v表是一个包含基类(即父类)中所有虚函数的映射。这样就能在运行时,将他们映射到正常的重写函数上。
13.接口
定义:创建一个类,只由未实现的方法组成,子类去实现这些方法
14.可见性
C++中,可见性有3种:
- private: 本类+友元类 可以访问
- protect:本类+友元类+子类 可以访问
- public: 都可以访问
15.数组
15.1.创建数组
- 在栈上创建数组: int arr[5];
- 在堆上创建数组: int* arr = new int[5];
和构造函数用不用new一样:
栈上创建的数组自动内存管理 ,函数结束时自动释放内存
堆上创建的数组手动内存管理 :必须使用 delete[] 手动释放内存
15.2.间接寻址
定义:有一个指针指向了一个内存块地址,而这个内存块保存了实际的数组
在class中使用new(堆上)创建数组, 可以看到指针example指向的是一个内存地址,而这个地址里面保存的才是数组
在class中不使用new(栈上)创建数组,指针example地址里面保存的就是数组
15.3.获取数组元素数量
int array[5];
int count = sizeof(array) / sizeof(int) // 5
int* array = new int[5];
int count = sizeof(array) / sizeof(int) // 1/2
对指针使用 sizeof 操作符只会返回指针本身的大小(通常在32位系统上是4字节,在64位系统上是8字节),而不是它指向的内存区域的大小。所以第2个 sizeof(array) 返回的是指针的大小,除以 sizeof(int) 后得到的值通常是1或2。
因此通过sizeof(array) / sizeof(int)是容易造成错误的
推荐方式:
-
使用static const变量
-
使用C++11中的std::array
16.字符串
字符串就是字符数组,字符串从指针的内存地址开始,继续直至碰到0(0被称为空终止符),也就是字符串结束了。
下面的例子因为没有加空终止符,所以字符串一直往后走,直到碰到了0
看一个正常的字符串:
字符串也可以使用std::string
#include <iostream>
int main() {
std::string name = "hello";
std::cout << name << std::endl;
std::string hello = std::string("hello") + "world"; // 追加字符串方式1
name += "world"; // 追加字符串方式2
std::cin.get();
}
有一点需要注意:方法中传参是字符串时,应该使用引用或者指针,如果直接传字符串,比如下面的追加字符串操作,会新生成一个字符串,这是比较耗时的。
name和str的内存地址以及对应的字符串都是不同的,可以看到新生成了一个字符串。
修改上面代码,使用引用:
17.const
const变量
类似于Java final用于变量时,就是常量了,不能再赋值了
int main() {
const int a = 5;
a = 10; // 报错:E1037: expression must be a modifiable lvalue
}
const指针
#include <iostream>
int main() {
int a = 5;
int * prt = new int;
*prt = 10;
prt = &a;
std::cin.get();
}
没有用const时,我们可以修改指针指向的值的数*prt, 也可以改变指针的指向prt = &a。
const指针需要分3种情况
- const在*之前
- const在*之后
- const在*前后
就按顺序来看看吧,
const在*之前
#include <iostream>
int main() {
int a = 5;
int const* prt = new int;
*prt = 10; // 报错:E1037: expression must be a modifiable lvalue
prt = &a;
std::cin.get();
}
和上面代码唯一不同,就是在*前面加了const, * prt = 10就会报错,也就是说const在 *之前时,const指针指向的内存地址中存的数是不能改变了,即指针常量。
需要说明的一点:const int* 和 int const*是完全一样的
const在*之后
#include <iostream>
int main() {
int a = 5;
int* const prt = new int;
*prt = 10;
prt = &a; // 报错:E1037: expression must be a modifiable lvalue
std::cin.get();
}
在* 后面加const,prt = &a就会报错,也就是说const在*之后时,const指针指向就不能改变了,即常量指针。
const在*前后
#include <iostream>
int main() {
int a = 5;
int const* const prt = new int;
*prt = 10; // 报错:E1037: expression must be a modifiable lvalue
prt = &a; // 报错:E1037: expression must be a modifiable lvalue
std::cin.get();
}
在*前面和后面加const, * prt = 10和prt = &a就会报错,也就是说const在 *前后时,const指针指向的内存地址中存的数和const指针指向都不能改变了。
const引用
#include <iostream>
int main() {
int b = 2;
int const &a = b;
a = 3; // 报错:E1037: expression must be a modifiable lvalue
}
const引用只有一种,const在&之前。我们之前已经接触过引用了:引用必须初始化,不可改变绑定。在加上const之后,其实就跟const int* const一样——不可改变绑定也不能改变地址中的值。
const方法
const用于方法时,说明这个方法不会做任何修改,是只读不写的。
举个栗子,现在我们有一个常量引用或常量指针
const Entry*代表常量指针,地址里面的内容没法修改了。而const就是加了这个限制。如果没有这里限制 ,会报错E1086: the object has type qualifiers that are not compatible with the member function "Entry:.Getld" object type is: const Entry
其实C++也给了解除const限制的关键字——mutable
这样就不会报错了
需要注意一点的是 const只能用于类中的方法
18.mutable
在上面const方法中用到了mutable,那就顺势来看看mutable有什么用吧
- const + mutable
- lambda + mutable
const + mutable在上一点介绍const时已经说过了,就是解除const限制。
我们来看看lambda + mutable
在lambda中,x++, 现在会报错E0137: expression must be a modifiable lvalue
添加mutable后代码如下:
运行正常,输出
f.x: 9
main.x: 8
可以看到,这样是值传递,我们也看下引用传递:
运行正常,输出
f.x: 9
main.x: 9
19.成员初始化列表
我们先来看下不使用成员初始化列表
输出
Entry: Default
Entry: Sample
再来看看使用成员初始化列表
输出
Entry: Default
Entry: Sample
可以看到上面两种都是同样的效果,那么两种方式有什么区别呢,或者说什么时候用成员初始化列表,什么时候不用成员初始化列表呢
再来了看看另外的一个栗子
输出
Example constructor called!
Example constructor called: 3
只调用了一次Example(3), 居然把Example的有参和无参构造函数都调用了。
原因是在进入构造函数体之前,所有成员变量会先被默认初始, m_example被默认初始化,调用了Example的无参构造函数。然后执行构造函数体内的代码m_example = Example(3); 创建一个新的Example对象并赋值给已初始化的m_example。
使用成员初始化列表来解决这个问题
这样就只会输出Example constructor called: 3
注意:成员初始化列表顺序要和成员变量顺序一致,比如上面代码中先定义了m_name,那在成员初始化列表也要先给m_name赋值
20.隐式转换、explicit
上面代码e0报错,e1,e2正常
隐私转换就是如上面代码,因为Entry有std::string和int这两种构造函数,直接使用对应参数,会隐私转换。比如 Entry e1 = std::string("22");就相当于Entry e1 = Entry(std::string("22"));
而Entry e0 = "22";会报错,是因为隐式转换只会转换一次,"22" -> std::string("22") -> Entry(std::string("22")) 这种就需要两次
explicit就是禁止隐式转换,我们来看看代码
代码只是在Entry的构造函数前面加了explicit,隐式转换就不能用了
21.运算符及其重载
重载了运算符+, *, ==
22.智能指针
使用new在堆上分配内存,需要delete来删除,释放内存,因为不会自动化释放内存。智能指针就是实现这一过程自动化的一种方式。
智能指针本质上是一个原始指针的包装。当创建一个智能指针,它会调用new为你分配内存,然后基于你使用的智能指针,这些内存会在某一时刻自动释放。
unique_ptr
unique_ptr是作用域指针,超出作用域时,它会被销毁,然后调用datele。
注意: 无法复制unique_ptr,如果复制一个unique_ptr, 复制的这个unique_ptr和原来的unique_ptr会指向同一个内存块。如果其中一个挂了,他会释放内存,那么指向同一内存的另外一个unique_prt就指向了已经被释放的内存。因此不能复制unique_ptr
当代码运行到entity所在的大括号之外时,会自动调用entity析构函数
shared_ptr
std::shared_ptr 是 C++ 标准库中的智能指针,它实现了引用计数的内存管理机制。
引用计数 :
- 每个 shared_ptr 对象内部维护两个指针:一个指向实际对象,一个指向控制块
- 控制块包含引用计数和其他管理信息
- 当创建新的 shared_ptr 指向同一对象时,引用计数加1
- 当 shared_ptr 被销毁或重新赋值时,引用计数减1
内存布局
shared_ptr 对象
- 指向实际对象的指针
- 指向控制块的指针(引用计数、弱引用计数、自定义删除器(可选))
销毁时机 shared_ptr 管理的对象会在以下情况被销毁:
- 当最后一个指向该对象的 shared_ptr 被销毁或重新赋值时(引用计数变为0)
- 具体步骤:
- shared_ptr 析构函数被调用
- 引用计数减1
- 如果引用计数变为0,则调用删除器销毁对象
- 如果弱引用计数也为0,则销毁控制块
上面代码是可以运行的,也就是说shared_ptr正如名字一样是可以shared的。当代码运行到std::shared_ptr e0所在的大括号外面(第2处)时,会自动调用Entity的析构函数。
weak_ptr
运行到第1处时会自动调用Entity的析构函数。因为weak_ptr不会增加引用
优先使用unique_ptr;需要在对象之间共享时,使用shared_ptr;在对象之间共享但不需要增加引用时,使用weak_ptr
23.复制与拷贝函数
上面代码会输出
2 3
5 3
这里就发生了复制,e0和e1是两个不同的变量,修改e1不会影响到e0。使用指针可以让e0和e1指针同一变量,就可以避免发生复制
上面代码会输出
5 3
5 3
这里e0和e1都是指针,并且指向了同一地址。
上面代码中,我们自定义了String类,运行发现会打印两次delete,并且报错。是因为这里是浅拷贝,str和str2中存的m_Buffer是指针,指向的是同一地址,修改一个也会影响另外一个,str执行析构函数时,已经把m_Buffer中的数据销毁了,str2就无法再次销毁了,试图两次释放同一内存块,这是没法做到的
通过添加断点方式,查看str和str2中m_Buffer的地址是一样的,验证了上面的说法。
下面看下如何通过拷贝构造函数来进行深拷贝
String(const String& other): m_Size(other.m_Size)和它大括号里面的就是拷贝构造函数,同样还是查看此种方式str和str2中的m_Buffer的地址
可以看到str和str2中的m_Buffer的地址是不同的,说明已经是深拷贝了,修改其中一个,不会影响另外一个了。
24.箭头操作符
基本用法
重载箭头操作符
获取内存中某个成员变量的偏移量
25.动态数组(std::vector)的使用
printEntry和for (Entry& e: entries) 都使用引用传递,如果使用值传递,会发生复制。
注意:C++中std::vector是可以使用基本数据类型的,比如
std::vector<int> data;
std::vector<float> data;
26.方法中返回多个值
方法中传入多个引用
方法中传入多个指针
这个和多个引用的区别是,参数可以传nullptr
方法返回数组
也可以使用std::vector
std::array和std::vector的区别:array在栈上创建,vector会把它的底层存储在堆上,因此std::array更快
方法返回std::tuple
方法返回std::pair
std::pair可以使用first和second获取第一个,第二个值,也可以使用std::get的方式
方法返回自定义结构体(推荐方式)
C++17及以上可以使用结构化绑定,通过auto[xxx, yyy...]的方式(推荐方式)
27.模板
定义:C++模板(Template)是一种支持泛型编程的机制。它允许你编写与类型无关的代码,在编译时由编译器根据实际类型自动生成对应的代码
函数模板
类模板
可以在使用Array类时,再指定数组类型
28.宏
通过自定义的DEBUG,可以在Debug版本打印日志,Release版本不打印日志
可以使用if来实现上面同样的作用
宏定义有多行代码,使用\,相当于enter
29.auto关键字
使用auto的好处是:当api返回类型发生改变时,不用修改代码;坏处则是当返回类型改变时,有的方法无法调用,而且需要手动查看变量类型。
auto推荐使用在类型过长时
30.静态数组(std::array)
静态数组定义:不增长的数组。创建时就决定了数组元素类型和大小,之后不能再改变。
使用std::array的好处:
-
有边界检查
-
std::array可以用大量的STL(标准模板库)算法
-
可以用户std::array.size()获取大小
31.函数指针
定义: 函数指针是将一个函数赋值给一个变量的方法
作用:将函数作为参数传给其他函数
函数指针定义方式
- 直接用auto xxx = 函数名
- void(*xxx)(参数)
- 使用typedef void(*yyy)(参数); 再用yyy xxx = 函数名
调用函数指针对应的函数都是通过xxx(参数)的方式
上面代码就将PrintNumber函数作为参数传给了ForEach函数
32.lambda
lambda本质上是定义匿名函数的一种方式
int count [=](int value) {std::cout << "number" << count << std::endl;}; 中括号:用来传递lambda之外的变量,有两种方式:
- 值传递:使用=/变量名
- 引用传递:使用&
如果要在lambda里面修改外部传进来的变量,需要使用mutable
33.suing namespace
34.名称空间
namespace用来避免方法名冲突,定义之后通过定义的名称::方法名来调用方法,如果不使用名称空间,可以直接修改方法名,使用xxx_yyy的方式,比如上面的apple_print
当命名空间有多个方法时,可以通过using关键字,来直接使用部分方法
命名空间存在多层时,可以用namespace xxx = ...或using namespace ...来,快速访问其中的方法,但要注意 在用namespace xxx = ...或using namespace ...,尽量限制在一个小的作用域下,永远不要在头文件中使用。
35.线程
打印出的线程id不一样,说明是不同的线程
36.计时
Timer类在构造函数中记录开始时间,在构造函数中计算持续时间。
如果是使用Timer(); 只是创建了一个临时的 Timer 对象,并没有给它分配一个变量名。 在C++中,这种临时对象的生命周期非常短: 它会在该语句结束后立即被析构。
37.多维数组
定义:指具有两个或两个以上索引的数组数据结构。它可以看作是元素为数组的一维数组,因此常见的二维数组可以理解为“数组的数组”。多维数组常用于表示矩阵、表格、图像等需要多重索引的数据结构。
以二维数组为例,可以用A[i][j]来访问第i行第j列的元素。三维数组则可以用A[i][j][k]来访问。
在C++中,二维数组是将一维数组的指针地址存在连续的内存中
在上面代码中,定义了一个二维数组。查看它的内存地址。
可以看到arr的内存地址是0x0000014A5A6046B0,它里面存的值是并不是2,而是3个一维数组的内存地址, 60 87 60 5a 4a 01 00 00 d0 80 60 5a 4a 01 00 00 70 83 60 5a 4a 01 00 00。由于小端序(不清楚可以看第3小节指针),它们的内存地址分别是0x0000014A5A608370,0x0000014A5A6080D0,0x0000014A5A608760。我们来看看这3个内存地址中存了什么
0x0000014A5A608370:
0x0000014A5A6080D0:
0x0000014A5A608760
可以看到这3个内存地址中,就是3个数组的值,在代码中赋值为2,所以这里也都是存的2。
二维数组是存的一维数组的内存地址。以此类推,其实三维数组就是存的二维数组的内存地址,四维数组就是...
38.类型双关
定义:通过一种类型的对象的内存表示,来访问或解释为另一种类型。
常见实现方式
1.使用指针强制类型转换
通过指针将一种类型的地址解释为另一种类型
此种方式常见但违反了严格别名规则(strict aliasing rule),可能导致编译器优化出错。
2.使用联合体(union)
联合体的所有成员共享同一块内存,可以通过写入一种类型的数据,再以另一种类型读取,实现类型双关。
u.i 写入一个整数,然后通过 u.f 以float类型读取,实现了类型双关
3.使用memcpy(推荐)
现代C++推荐用 memcpy 实现类型双关,既安全又高效
39.联合体
定义:一种特殊的自定义数据类型,它允许在同一块内存区域存储不同的数据类型,但同一时刻只能存储其中的一种类型。
特定:
- 节省内存 :所有成员共用一块内存,大小等于最大成员的大小。
- 同一时刻只能存储一个成员的值 :赋值给某个成员后,其他成员的值会被覆盖。
- 常用于类型双关、协议解析、硬件寄存器映射等场景。
修改i,f,c中的任意一个,都会导致另外两个输出发生变化
40.虚析构函数
Base* baseDerived = new Derived(); delete baseDerived;
可以看到当执行delete baseDerived,只调用了Base类的析构函数,却没有调用Derived析构函数。这样就会造成内存泄漏。使用虚析构函数修改代码,添加virtual关键字。
使用了虚析构函数,delete baseDerived就会调用了Base类的析构函数和Derived的析构函数。
注意:当一个类有子类,则这个类需要使用虚析构函数。
41.类型转换
1.隐式类型转换(自动类型转换)
编译器自动完成的类型转换
2. 显式类型转换(强制类型转换)
C风格强制类型转换
3.C++风格类型转换
C++提供了四种类型安全的转换方式:
3.1.static_cast
用于大多数类型转换(如基本类型、类层次间的指针/引用转换等):
3.2.const_cast
用于去除或添加const/volatile限定符:
3.3.dynamic_cast
用于多态类型之间的安全向下转换(基类指针/引用转派生类):
3.4.reinterpret_cast
用于底层指针、整数等类型的强制转换,慎用:
使用C++风格类型转换的好处
-
编译时检查,减少在尝试强制转换时犯的错误,如类型不兼容
-
可以在代码库中搜索相关代码,对阅读和编写代码有帮助
42.std::variant
C++17及以上可以用std::variant<>存放多类型数据。<>中是用来说明可能存放的数据类型。
联合体相对来说更省空间,但std::variant更加类型安全。因此优先使用 std::variant来保证类型安全,除非有严格的内存限制。
最后
感谢阅读,希望本文对你有所帮助,如有任何不对的地方,欢迎大家指正。