C++学习笔记

1,207 阅读30分钟

学习Android源码时,需要一定的C++基础,因此就有了这份C++学习笔记。

我是找的视频学习的,先贴下视频链接:

  1. 油管
  1. b站:

1.Hello World!(C++)

按照惯例,先来个Hello World! image.png

运行代码会输出Hello World!到控制台。 return 0;这行是可以省略的,C++中的mian函数是一个特例,如果没有任何返回,系统将视为返回0

2.预处理语句

2.1.#include

在C++中,通常的代码组织方式是:

  1. 头文件(.h/.hpp) :

    • 包含函数声明(也称为函数原型)
    • 包含类定义
    • 包含宏定义
    • 包含类型定义
  2. 源文件(.cpp) :

    • 包含函数的具体实现(定义)
    • 包含变量的定义

也就是说

  1. 在头文件中进行 声明 (declaration)
  2. 在源文件中进行 定义 (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文件 image.png

执行main函数,最开始会跑到FirstStageMain中,在first_stage_init.h中声明了这个函数 image.png

在first_stage_init.cpp中定义了这个函数 image.png

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.指针

定义:指针是一个地址,一个保存内存地址的整数

注意:所有类型的指针都是保存内存地址的整数

举个栗子

image.png

当想知道一个变量在内存中的地址,可以使用&运算符,就跟上面代码中一样。那我们来看看prt这个指针的值是多少

image.png

可以清楚的看到: prt指针的地址是0x000000E2A854FAC0, 这个内存地中存的值是5。

不用将指针想的太多麻烦,指针就是一个内存地址整数


那么指针的指针又是什么呢? 再来个栗子 image.png

image.png

image.png

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 100x00000065D44FF910

4.引用

定义:引用只是指针的伪装,只是指针的语法糖

注意:引用不是新的变量,不占用内存

4.1.举个栗子

image.png

上面代码会输出Value of a: 10

在这里,ref就是a的一个别名,修改ref,也就是修改a

4.2.再来个栗子

image.png

上面代码会输出Value of a: 5

在Increment函数中的参数是值传递。调用函数时,实参 a 的值会被复制一份给形参 value, 函数内修改的只是这个副本,不影响原始变量a。

4.3.栗子3 使用指针

image.png

上面代码会输出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 使用引用

image.png

这同样会输出Value of a: 6

现在再看看这句话:引用只是指针的伪装,只是指针的语法糖

比如下面使用引用和指针都能修改变量的值

使用指针:

image.png

使用引用:

image.png

需要注意的是:一旦声明了一个引用,就不能改变它引用的对象

image.png

这里会输出

Value of a: 8 Value of b: 8

首先会声明a的引用ref,然后赋值为了b,所以a,b的值都是8。

如果想先指向a,然后改为指向b

image.png

这里会输出

Value of a: 1

Value of b: 2

引用和指针的区别

  • 引用必须初始化,不可改变绑定

  • 指针可以改变绑定

5.类

定义:数据和处理数据的函数组合在一起 image.png

这里会输出

Value of player.x: 2

Value of player.y: 2

6.结构体

还是上面的代码,直接将class改为struct,并删除public image.png

这里同样会输出

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

image.png

在结构体中定义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赋值是没什么意义的,可以使用下面方式 image.png

那什么时候应该在类和结构体中定义静态变量?

  • 想要在所有实例间共享数据
  • 这个变量储存在类或结构体中是有意义,与类和结构体有关

再看看静态方法

#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 对局部变量)的作用

  1. 生命周期延长
    • 普通局部变量:函数调用时创建,函数返回时销毁
    • static局部变量:在程序启动时创建,程序结束时销毁
  2. 保持状态:static局部变量的值会在多次函数调用间保持
  3. 存储位置:从栈内存改为静态存储区

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.继承

image.png

Man类继承了Person类,那么父类中的方法和变量(非private时)就可以调用,如果子类对象没有重写父类方法则调用父类方法,重写了则调用自己的方法。

12.虚函数

image.png

前两个打印是正常的,我们通过创建对象,去调用各自的方法。但最后一个打印却跟预期是不同的,声明是Person类,但它的类型是Man类,应该调用Man类的方法,实际上调用的却是Person类。

原因:在声明函数时,方法通常在类内部。当调用方法时,会调用属于该类的方法

这就引出了本小节的虚函数。

虚函数引入了Dynamic Dispatch(动态联编),通常通过v表(虚函数表)来实现编译,v表是一个包含基类(即父类)中所有虚函数的映射。这样就能在运行时,将他们映射到正常的重写函数上。 image.png

13.接口

定义:创建一个类,只由未实现的方法组成,子类去实现这些方法 image.png

14.可见性

C++中,可见性有3种:

  • private: 本类+友元类 可以访问
  • protect:本类+友元类+子类 可以访问
  • public: 都可以访问

15.数组

15.1.创建数组

  • 在栈上创建数组: int arr[5];
  • 在堆上创建数组: int* arr = new int[5];

和构造函数用不用new一样:

栈上创建的数组自动内存管理 ,函数结束时自动释放内存

堆上创建的数组手动内存管理 :必须使用 delete[] 手动释放内存

15.2.间接寻址

定义:有一个指针指向了一个内存块地址,而这个内存块保存了实际的数组

image.png

在class中使用new(堆上)创建数组, 可以看到指针example指向的是一个内存地址,而这个地址里面保存的才是数组

image.png

在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变量 image.png

  • 使用C++11中的std::array image.png

16.字符串

字符串就是字符数组,字符串从指针的内存地址开始,继续直至碰到0(0被称为空终止符),也就是字符串结束了。

下面的例子因为没有加空终止符,所以字符串一直往后走,直到碰到了0

image.png

看一个正常的字符串:

image.png

字符串也可以使用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();
}

有一点需要注意:方法中传参是字符串时,应该使用引用或者指针,如果直接传字符串,比如下面的追加字符串操作,会新生成一个字符串,这是比较耗时的。

image.png

image.png

name和str的内存地址以及对应的字符串都是不同的,可以看到新生成了一个字符串。

修改上面代码,使用引用:

image.png

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方法

image.png

const用于方法时,说明这个方法不会做任何修改,是只读不写的。

举个栗子,现在我们有一个常量引用或常量指针 image.png

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 image.png

这样就不会报错了

需要注意一点的是 const只能用于类中的方法 image.png

18.mutable

在上面const方法中用到了mutable,那就顺势来看看mutable有什么用吧

  • const + mutable
  • lambda + mutable

const + mutable在上一点介绍const时已经说过了,就是解除const限制。

我们来看看lambda + mutable image.png

在lambda中,x++, 现在会报错E0137: expression must be a modifiable lvalue

添加mutable后代码如下: image.png

运行正常,输出

f.x: 9

main.x: 8

可以看到,这样是值传递,我们也看下引用传递: image.png

运行正常,输出

f.x: 9

main.x: 9

19.成员初始化列表

我们先来看下不使用成员初始化列表 image.png

输出

Entry: Default

Entry: Sample

再来看看使用成员初始化列表 image.png

输出

Entry: Default

Entry: Sample

可以看到上面两种都是同样的效果,那么两种方式有什么区别呢,或者说什么时候用成员初始化列表,什么时候不用成员初始化列表呢

再来了看看另外的一个栗子 image.png 输出

Example constructor called!

Example constructor called: 3

只调用了一次Example(3), 居然把Example的有参和无参构造函数都调用了。

原因是在进入构造函数体之前,所有成员变量会先被默认初始, m_example被默认初始化,调用了Example的无参构造函数。然后执行构造函数体内的代码m_example = Example(3); 创建一个新的Example对象并赋值给已初始化的m_example。

使用成员初始化列表来解决这个问题 image.png 这样就只会输出Example constructor called: 3

注意:成员初始化列表顺序要和成员变量顺序一致,比如上面代码中先定义了m_name,那在成员初始化列表也要先给m_name赋值

20.隐式转换、explicit

image.png

上面代码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就是禁止隐式转换,我们来看看代码 image.png 代码只是在Entry的构造函数前面加了explicit,隐式转换就不能用了

21.运算符及其重载

image.png

重载了运算符+, *, ==

22.智能指针

使用new在堆上分配内存,需要delete来删除,释放内存,因为不会自动化释放内存。智能指针就是实现这一过程自动化的一种方式。

智能指针本质上是一个原始指针的包装。当创建一个智能指针,它会调用new为你分配内存,然后基于你使用的智能指针,这些内存会在某一时刻自动释放。

unique_ptr

unique_ptr是作用域指针,超出作用域时,它会被销毁,然后调用datele。

注意: 无法复制unique_ptr,如果复制一个unique_ptr, 复制的这个unique_ptr和原来的unique_ptr会指向同一个内存块。如果其中一个挂了,他会释放内存,那么指向同一内存的另外一个unique_prt就指向了已经被释放的内存。因此不能复制unique_ptr image.png 当代码运行到entity所在的大括号之外时,会自动调用entity析构函数

shared_ptr

std::shared_ptr 是 C++ 标准库中的智能指针,它实现了引用计数的内存管理机制。

引用计数 :

  • 每个 shared_ptr 对象内部维护两个指针:一个指向实际对象,一个指向控制块
  • 控制块包含引用计数和其他管理信息
  • 当创建新的 shared_ptr 指向同一对象时,引用计数加1
  • 当 shared_ptr 被销毁或重新赋值时,引用计数减1

内存布局

shared_ptr 对象

  • 指向实际对象的指针
  • 指向控制块的指针(引用计数、弱引用计数、自定义删除器(可选))

销毁时机 shared_ptr 管理的对象会在以下情况被销毁:

  1. 当最后一个指向该对象的 shared_ptr 被销毁或重新赋值时(引用计数变为0)
  2. 具体步骤:
    • shared_ptr 析构函数被调用
    • 引用计数减1
    • 如果引用计数变为0,则调用删除器销毁对象
    • 如果弱引用计数也为0,则销毁控制块

image.png 上面代码是可以运行的,也就是说shared_ptr正如名字一样是可以shared的。当代码运行到std::shared_ptr e0所在的大括号外面(第2处)时,会自动调用Entity的析构函数。

weak_ptr

image.png 运行到第1处时会自动调用Entity的析构函数。因为weak_ptr不会增加引用

优先使用unique_ptr;需要在对象之间共享时,使用shared_ptr;在对象之间共享但不需要增加引用时,使用weak_ptr

23.复制与拷贝函数

image.png

上面代码会输出

2 3

5 3

这里就发生了复制,e0和e1是两个不同的变量,修改e1不会影响到e0。使用指针可以让e0和e1指针同一变量,就可以避免发生复制

image.png

上面代码会输出

5 3

5 3

这里e0和e1都是指针,并且指向了同一地址。

image.png

上面代码中,我们自定义了String类,运行发现会打印两次delete,并且报错。是因为这里是浅拷贝,str和str2中存的m_Buffer是指针,指向的是同一地址,修改一个也会影响另外一个,str执行析构函数时,已经把m_Buffer中的数据销毁了,str2就无法再次销毁了,试图两次释放同一内存块,这是没法做到的

image.png

通过添加断点方式,查看str和str2中m_Buffer的地址是一样的,验证了上面的说法。

下面看下如何通过拷贝构造函数来进行深拷贝

image.png

String(const String& other): m_Size(other.m_Size)和它大括号里面的就是拷贝构造函数,同样还是查看此种方式str和str2中的m_Buffer的地址

image.png

可以看到str和str2中的m_Buffer的地址是不同的,说明已经是深拷贝了,修改其中一个,不会影响另外一个了。

24.箭头操作符

基本用法

image.png

重载箭头操作符

image.png

获取内存中某个成员变量的偏移量

image.png

25.动态数组(std::vector)的使用

image.png

printEntry和for (Entry& e: entries) 都使用引用传递,如果使用值传递,会发生复制。

注意:C++中std::vector是可以使用基本数据类型的,比如

std::vector<int> data;
std::vector<float> data;

26.方法中返回多个值

方法中传入多个引用

image.png

方法中传入多个指针

image.png 这个和多个引用的区别是,参数可以传nullptr

方法返回数组

image.png

也可以使用std::vector image.png

std::array和std::vector的区别:array在栈上创建,vector会把它的底层存储在堆上,因此std::array更快

方法返回std::tuple

image.png

方法返回std::pair

image.png std::pair可以使用first和second获取第一个,第二个值,也可以使用std::get的方式

方法返回自定义结构体(推荐方式)

image.png

C++17及以上可以使用结构化绑定,通过auto[xxx, yyy...]的方式(推荐方式)

image.png

27.模板

定义:C++模板(Template)是一种支持泛型编程的机制。它允许你编写与类型无关的代码,在编译时由编译器根据实际类型自动生成对应的代码

函数模板

image.png

类模板

image.png

可以在使用Array类时,再指定数组类型 image.png

28.宏

image.png

image.png image.png 通过自定义的DEBUG,可以在Debug版本打印日志,Release版本不打印日志

image.png image.png 可以使用if来实现上面同样的作用

image.png

宏定义有多行代码,使用\,相当于enter

29.auto关键字

image.png

image.png

image.png

使用auto的好处是:当api返回类型发生改变时,不用修改代码;坏处则是当返回类型改变时,有的方法无法调用,而且需要手动查看变量类型。

auto推荐使用在类型过长时 image.png

30.静态数组(std::array)

静态数组定义:不增长的数组。创建时就决定了数组元素类型和大小,之后不能再改变。 image.png

使用std::array的好处:

  • 有边界检查

  • std::array可以用大量的STL(标准模板库)算法

  • 可以用户std::array.size()获取大小

31.函数指针

定义: 函数指针是将一个函数赋值给一个变量的方法

作用:将函数作为参数传给其他函数 image.png

函数指针定义方式

  • 直接用auto xxx = 函数名
  • void(*xxx)(参数)
  • 使用typedef void(*yyy)(参数); 再用yyy xxx = 函数名

调用函数指针对应的函数都是通过xxx(参数)的方式

image.png

上面代码就将PrintNumber函数作为参数传给了ForEach函数

32.lambda

lambda本质上是定义匿名函数的一种方式 image.png

int count [=](int value) {std::cout << "number" << count << std::endl;}; 中括号:用来传递lambda之外的变量,有两种方式:

  • 值传递:使用=/变量名
  • 引用传递:使用&

如果要在lambda里面修改外部传进来的变量,需要使用mutable

33.suing namespace

image.png

34.名称空间

image.png

namespace用来避免方法名冲突,定义之后通过定义的名称::方法名来调用方法,如果不使用名称空间,可以直接修改方法名,使用xxx_yyy的方式,比如上面的apple_print

image.png

当命名空间有多个方法时,可以通过using关键字,来直接使用部分方法

image.png

命名空间存在多层时,可以用namespace xxx = ...或using namespace ...来,快速访问其中的方法,但要注意 在用namespace xxx = ...或using namespace ...,尽量限制在一个小的作用域下,永远不要在头文件中使用。

35.线程

image.png 打印出的线程id不一样,说明是不同的线程

36.计时

image.png

image.png Timer类在构造函数中记录开始时间,在构造函数中计算持续时间。 如果是使用Timer(); 只是创建了一个临时的 Timer 对象,并没有给它分配一个变量名。 在C++中,这种临时对象的生命周期非常短: 它会在该语句结束后立即被析构。

37.多维数组

定义:指具有两个或两个以上索引的数组数据结构。它可以看作是元素为数组的一维数组,因此常见的二维数组可以理解为“数组的数组”。多维数组常用于表示矩阵、表格、图像等需要多重索引的数据结构。

以二维数组为例,可以用A[i][j]来访问第i行第j列的元素。三维数组则可以用A[i][j][k]来访问。

在C++中,二维数组是将一维数组的指针地址存在连续的内存中 image.png

在上面代码中,定义了一个二维数组。查看它的内存地址。

image.png 可以看到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: image.png

0x0000014A5A6080D0: image.png

0x0000014A5A608760 image.png 可以看到这3个内存地址中,就是3个数组的值,在代码中赋值为2,所以这里也都是存的2。

二维数组是存的一维数组的内存地址。以此类推,其实三维数组就是存的二维数组的内存地址,四维数组就是...

38.类型双关

定义:通过一种类型的对象的内存表示,来访问或解释为另一种类型。

常见实现方式

1.使用指针强制类型转换

通过指针将一种类型的地址解释为另一种类型 image.png 此种方式常见但违反了严格别名规则(strict aliasing rule),可能导致编译器优化出错。

2.使用联合体(union)

联合体的所有成员共享同一块内存,可以通过写入一种类型的数据,再以另一种类型读取,实现类型双关。 image.png

u.i 写入一个整数,然后通过 u.f 以float类型读取,实现了类型双关

3.使用memcpy(推荐)

现代C++推荐用 memcpy 实现类型双关,既安全又高效 image.png

39.联合体

定义:一种特殊的自定义数据类型,它允许在同一块内存区域存储不同的数据类型,但同一时刻只能存储其中的一种类型。

特定:

  • 节省内存 :所有成员共用一块内存,大小等于最大成员的大小。
  • 同一时刻只能存储一个成员的值 :赋值给某个成员后,其他成员的值会被覆盖。
  • 常用于类型双关、协议解析、硬件寄存器映射等场景。 image.png

修改i,f,c中的任意一个,都会导致另外两个输出发生变化

40.虚析构函数

image.png image.png

Base* baseDerived = new Derived(); delete baseDerived;

可以看到当执行delete baseDerived,只调用了Base类的析构函数,却没有调用Derived析构函数。这样就会造成内存泄漏。使用虚析构函数修改代码,添加virtual关键字。

image.png 使用了虚析构函数,delete baseDerived就会调用了Base类的析构函数和Derived的析构函数。

注意:当一个类有子类,则这个类需要使用虚析构函数。

41.类型转换

1.隐式类型转换(自动类型转换)

编译器自动完成的类型转换 image.png

2. 显式类型转换(强制类型转换)

C风格强制类型转换

image.png

3.C++风格类型转换

C++提供了四种类型安全的转换方式:

3.1.static_cast

用于大多数类型转换(如基本类型、类层次间的指针/引用转换等): image.png

3.2.const_cast

用于去除或添加const/volatile限定符: image.png

3.3.dynamic_cast

用于多态类型之间的安全向下转换(基类指针/引用转派生类): image.png

3.4.reinterpret_cast

用于底层指针、整数等类型的强制转换,慎用:
image.png

使用C++风格类型转换的好处

  • 编译时检查,减少在尝试强制转换时犯的错误,如类型不兼容

  • 可以在代码库中搜索相关代码,对阅读和编写代码有帮助

42.std::variant

image.png

C++17及以上可以用std::variant<>存放多类型数据。<>中是用来说明可能存放的数据类型。

image.png image.png

联合体相对来说更省空间,但std::variant更加类型安全。因此优先使用 std::variant来保证类型安全,除非有严格的内存限制。

最后

感谢阅读,希望本文对你有所帮助,如有任何不对的地方,欢迎大家指正