课程地址:Youtube上TheCherno上传的视频
翻译:神经元猫的高质量翻译

31 How Strings Work in C++ (and how to use them)

新的C++标准舍弃了C风格的字符串:const char* name = “JKA” (不知道,反正VS2022还能用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() {
const char* name = "JKA"; // vs2017后"const char*"类型值不能用于初始化化"char*"
std::cout << name << std::endl;

int big = 9; // 栈内存守卫,后面也会加上cc字节
char name2[3] = { 'J', 'K', 'A' };
//char name2[4] = { 'J', 'K', 'A', 0 };
std::cout << name2 << std::endl;

std::cin.get();
}

// 输出
JKA
JKA烫烫烫烫?

在C++中,使用双引号引起来的值是const char*类型的。

从右往左看,const和char靠的更近,表示指向内容不可改变,但指针可以重新赋值,这属于底层const

C++中string

实际上有一个BasicString模版类,std::string是BasicString类的模版特化版本——将BasicString模版类中的模版参数设置为char,这叫 模版特化(template specialization)

把类(对象)传给一个函数时,实际在进行复制:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string> // 虽然包含 <iostream> 后能使用 std::string 应该是确定的,不过不一定等价于包含 <string>

int main() {
//std::string name = "Hello" + " JKA"; // const char* 数组没有加法
std::string name = std::string("Hello ") + "JKA";
if (name.find("JK") == std::string::npos) {
return -1;
}
std::cout << name << std::endl;
std::cin.get();
}

注意:name.find("JK") == std::string::npos 的写法。

32 String Literals in C++

字符串字面量 是在双引号之前的一串字符。

字符串字面量永远是存储在内存的.rodata部分,不能修改(右值),否则为未定义行为,可能无法编译/编译报错/运行时发现字符串并为被修改。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
// #include <string> // 虽包含 <iostream> 后能使用 std::string,但不等价于包含 <string>

int main() {
const char name[8] = "JK\0A"; // "JK"后面跟一个结束标志\0, 再跟一个字母'A'
std::cout << strlen(name) << std::endl;
std::cin.get();
}

// 输出
2

cout 对于 wchar_t*, char16_t*, char32_t* 都只会输出首地址, 如果想要输出 wchar_t* 的内容,那么需要用 std::wcout 。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>
#include <stdlib.h>

int main() {
const char* name = u8"JKA";
const wchar_t* name2 = L"JKA"; // 宽字符:不同环境字节数不同,windows下2字节, Linux下4字节
const char16_t* name3 = u"JKA";
const char32_t* name4 = U"JKA";
std::cout << name << std::endl;
std::wcout << name2 << std::endl; // std::wcout
std::cin.get();
}

多行字符串的等价写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
const char* exp = R"(Line1
Line2
Line3
)";
std::cout << exp << std::endl;

const char* exp2 = "Line4\n"
"Line5\n"
"Line6\n";
std::cout << exp2 << std::endl;
std::cin.get();
}

33 CONST in C++

const是限定符,是伪关键字,因为它在改变生成代码方面什么也做不了。const有点像类和结构体的可见性,就像承诺——可以绕过或打破,这个承诺实际上可以简化很多代码。

💡 个人理解:const就像承诺,限制某个引用为const只是说通过这个引用不能改变,但可以存在其他内存别名是非const的,通过其他方式就能进行读写。

用法1:放在变量前面

用法2:与指针组合

int const*const int* 相同,在*前,能够让指针指向内容不能被修改(底层const)。
如果要让指针本身成为常量,需要放到后(顶层const)。

  • const int* a = new int;int const* a = new int;只是不能改变指针指向的内容,但是可以改变指针本身 a = (int*)&MAX_AGE;
  • int* const a = new int; 正好相反,可以改变指针指向的内容*a = 2;,但是不能把指针重新赋值。
  • const int* const a = new int; 二者都无法修改。

💡 这里更建议阅读《C++ Primer 第5版》P56,从右向左阅读,看const限定符距离指针变量更近,还是距离基本数据类型更近。理解后,可以发现这与Cherno讲的本质上是一样的。
——这种理解思路应该是根据运算符优先级 和 运算符结合顺序来的。

用法3:getter与setter

getter:将const 放在方法名的右边(只在类中有效),表示这个方法不会修改任何实际的类——只能读不能写,因此不能修改成员变量。

在类中,如果它们实际上没有修改类或者它们不应该修改类,应该总是将方法声明为const——否则在有常量引用或则类似的情况下,就用不了你的方法。

mutable 允许getter(常量方法)修改变量。

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
#include <iostream>
#include <string>

class Entity {
private:
int m_X, m_Y;
mutable int var; // mutabl使得const方法中仍然可修改此变量
public:
int GetX() const {
var = 2;
return m_X;
}

void SetX(int x) {
m_X = x;
}
};

void PrintEntity(const Entity& e) {
std::cout << e.GetX() << std::endl; // GetX()必须为const,否则会修改Entity
}

int main() {
Entity e;
const int* a = new int; // 底层const
a = nullptr;
//*a = 1; // const 放*前就不能修改指针指向内容,但可改指针本身

}

34 The Mutable Keyword in C++

mutable(mutable 可改变的,immutable 意为不可改变的 )实际上有两种不同的用途:

  1. 修饰class中成员变量,使其在const方法中可修改。(常用)
  2. 修饰lambda表达式,值捕获时可以直接操作传入参数。注意并非意味着引用捕获,依旧值捕获,不修改原值
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 <iostream>
#include <string>

class Entity {
private:
std::string m_Name;
mutable int m_DebugCount = 0;
public:
const std::string& GetName() const { // main中有const Entity则这里必须为const
++m_DebugCount;
return m_Name;
}
};

int main() {
const Entity e;
e.GetName();

int x = 8;
auto f = [=]() { // 按值传递,mutable声明后方可修改(按引用传递不用加)
++x;
std::cout << x << std::endl;
};
f();
return 0;
}

35 Member Initializer Lists in C++ (Constructor Initializer List)

构造函数初始化列表的顺序要与成员变量声明时的顺序一致,否则编译器会报警告,编译器是按照变量定义时顺序去初始化的。

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 Entity {
private:
int m_Score;
std::string m_Name;
public:
Entity()
: m_Score(0), m_Name("Unknown") {
}

Entity(const std::string& name)
: m_Score(0), m_Name(name) {
}

const std::string& GetName() const { // main中有const Entity则这里必须为const
return m_Name;
}
};

int main() {
const Entity e;
std::cout << e.GetName() << std::endl;
const Entity e1("JKA");
std::cout << e1.GetName() << std::endl;
return 0;
}

应该尽可能使用成员初始化列表。

const 对象只能调用 const 成员函数。
因为不能把 ClASS* 绑定到 const CLASS* 上——底层const不能乱绑定,顶层const没问题。

1
2
3
4
5
6
7
8
9
[cling]$ const int ca = 0;
[cling]$ int b = ca;
[cling]$ const int* cpa = nullptr;
[cling]$ int pb = cpa;
input_line_6:2:6: error: cannot initialize a variable of type 'int' with an lvalue of type 'const int *'
int pb = cpa;
^ ~~~
[cling]$ int* const cpa = nullptr;
[cling]$ int* pb = cpa;

而非 const 对象既是可以调用 const 成员函数的。

好处:除了让函数更简洁(构造函数体里不用写杂乱的成员变量初始化),还有功能上的区别:不使用的话会导致成员变量被初始化两次,浪费性能,如下:

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
class Example {
public:
Example() {
std::cout << "Created Entity!" << std::endl;
}

Example(const int x) {
std::cout << "Created Entity with " << x << std::endl;
}
};

class Entity {
private:
std::string m_Name;
Example exp; // 定义语句也会执行
public:
Entity()
: m_Name("Unknown") {
exp = Example(8); // 舍弃成员变量区域定义的Example,并创建一个性Example对象
}
};

int main() {
const Entity e;
return 0;
}

// 输出
Created Entity!
Created Entity with 8

应该将所有的成员变量初始化放在成员变量初始化列表中:: exp(8), m_Name("Unknown"): exp(Example(8)), m_Name("Unknown")

1
2
3
4
5
6
7
8
9
class Entity {
private:
std::string m_Name;
Example exp; // 定义语句也会执行
public:
Entity()
: m_Name("Unknown"), exp(8) {
}
};

36 Ternary Operators in C++ (Conditional Assignment)

三目运算符。

37 How to CREATE/INSTANTIATE OBJECTS in C++

两种创建对象的区别是 在哪块内存创建对象,尽量用栈 —— 1.性能:堆内存分配时间长,且由于cache等表现不好导致性能较差;2.堆上对象需要手动释放;但是栈通常小,比如1~2M,如果放不下就得放堆上。

  • Entity entity; 就可以了,会自动调用构造函数 或 Entity entity = Entity(”JKA”); 或者Entity entity(”JKA”)
    生存周期内存在,生存周期不一定是函数,还可以是if或空,即{}
  • Entity *entity = new Entity(”JKA”);

即使写一个完全为空的类,类中没有成员,也至少需要占用一个字节的内存。

38 The NEW Keyword in C++

new时会向OS要一块n字节的连续内存区域,OS找到后返回指针。
int *pa = new int;int *pa = new int[20];
Entity* e = new Entity() 的数组版本:Entity* e = new Entity[50];

空闲列表:维护有空闲字节的地址。

new是一个操作符,这意味着可以重载运算符改变其行为。

调用new = 调用隐藏其中的c函数 + 执行构造函数Entity* e = new Entity(); 等价于 Entity* e = (Entity*)malloc(sizeof(Entity)); + 执行构造函数; ,在C++中不应该写后者。

用 new 必须配套用 deletedelete[]

placement new: Entity* e = new(b) Entity(); 指定new的地址是使用变量b的地址。

39 Implicit Conversion and the Explicit Keyword in C++

C++允许编译器对代码执行一次隐式转换(隐式构造函数),而不需要用cast做强制转换。

explicit 关键字放在构造函数前,意味着没有隐式的转换,必须显式调用构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

class Entity {
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name)
: m_Name(name), m_Age(-1) {}

explicit Entity(const int age)
: m_Name("Unknown"), m_Age(age) {}
};

int main() {
//Entity a = "JKA"; // 不行: 从const char到string再到Entity需要2次转换
Entity a = std::string("JKA");
//Entity b = 22; // 不行: 加上explicit后就不能用了哟

std::cin.get();
}

40 OPERATORS and OPERATOR OVERLOADING in C++

运算符是一种通常代替一个函数执行一些事情的符号。.()newdelete 实际上都是是运算符,运算符实际就是函数。
尽量少用 运算符重载,除非确实适合。

重载本质是给运算符重载赋予新的含义。允许在程序中定义或更改运算符的行为,这在Java中不支持,C#中部分支持,而在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
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

struct Vector2 {
float x, y;
Vector2(float x, float y)
: x(x), y(y) {}

Vector2 Add(const Vector2& other) const {
return Vector2(x + other.x, y + other.y);
}

Vector2 operator+(const Vector2& other) const { // 可以调用普通函数
return Add(other);
}

Vector2 Multiply(const Vector2& other) const {
//return operator*(other); // 也可
return *this * other; // 普通函数也能调用 运算符重载函数
}

Vector2 operator*(const Vector2& other) const {
return Vector2(x * other.x, y * other.y);
}
};

// 运算符重载既可以放类内,也可以放类外
std::ostream& operator<<(std::ostream& stream, const Vector2& other) {
stream << other.x << "," << other.y;
return stream;
}

int main() {
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);

Vector2 res1 = position.Add(speed.Multiply(powerup));
Vector2 res2 = position + speed * powerup;

std::cout << res2 << std::endl; // Vector2无法直接输出,需要重载<< 或 to_string
std::cin.get();
}

// --- Out ---
4.55 5.65

💡 思考:为啥将operator+() 参数中const去掉后会报错:
error C2679: 二元“+”: 没有找到接受“Vector2”类型的右操作数的运算符(或没有可接受的转换)
error C2512: “Vector2”: 没有合适的默认构造函数可用?

原因:根据《C++ Primer 第5版》P202,函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。
这里返回的就是右值,而+若设置参数为非常量引用,则不能接受右值。
扩展:可以执行 Vector2 tmp = speed * powerup; 然后再 tmp.x++;,这是因为虽然*返回的是右值,但tmp是根据此右值初始化了一个变量。

41 The “this” keyword in C++

通过this关键字可以访问成员函数,可以在成员函数中使用this,this是一个指向当前对象实例的指针,访问成员变量时使用->,而非.

💡 this关键字存在的理由就是 不用每个对象都要申请空间去保存函数的声明和定义,只需要在类中保存一次,然后调用函数的时候传入不同的this就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void PrintEntity(Entity* e);

class Entity {
public:
int x, y;
Entity(int x, int y) { // 参数与类成员变量同名
this->x = x; // this是指针
this->y = y;
PrintEntity(this); // 传入当前对象指针

Entity& e = *this;
}

int GetX() const {
const Entity &e = *this; // 函数中需要声明为const,防止修改e
}
};

void PrintEntity(Entity* e) {
// Printing
}

上面例子中,如果用函数初始化列表就不用写this。

42 Object Lifetime in C++ (Stack/Scope Lifetimes)

在局部创建数组是一个典型的错误,一旦函数结束就超出了作用域。

目的:想在堆上分配,但在超出作用域时自动删除。
操作:使用标准库中**unique_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
#include <iostream>

class Entity {
public:
Entity() {
std::cout << "Created Entity!" << std::endl;
}

~Entity() {
std::cout << "Destroyed Entity!" << std::endl;
}
};

class ScopedPtr {
private:
Entity* m_Ptr;
public:
ScopedPtr(Entity* ptr)
: m_Ptr(ptr) {}

~ScopedPtr() {
delete m_Ptr;
}
};

int main() {
{
ScopedPtr e = new Entity(); // 隐式转换
//ScopedPtr e(new Entity());
} // 超出作用域, e析构
std::cout << "Main not end until here\n";
}

// --- Out ---
Created Entity!
Destroyed Entity!
Main not end until here

43 SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)

智能指针本质上是一个原始指针的包装,new后不需要手动delete,甚至可能new都不需要。

unique_ptr 开销小,优先选择

不能复制unique_ptr:复制的两个指针指向同一内存块,如果其中一个dead,内存就会被释放,这样指向同一内存的第二个unique_ptr指向已被释放的内存。
效率高、效率很高:上分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <memory> // 需要包含

class Entity {
public:
Entity() {
std::cout << "Created Entity!" << std::endl;
}

~Entity() {
std::cout << "Destroyed Entity!" << std::endl;
}
};

int main() {
{
//std::unique_ptr<Entity> entity(new Entity()); // 写法1
std::unique_ptr<Entity> entity = std::make_unique<Entity>(); // make_unique 异常安全
//std::unique_ptr<Entity> entity = new Entity(); // 不能这样, unique_ptr构造函数是explicit的
}
std::cin.get();
}

std::make_unique<类>()是在C++14引入的,C++11并不支持。
不直接调用new的原因是为了 异常安全

shared_ptr

实现方式取决于 编译器在编译器中使用的标准库,比如引用计数(reference counting)——引用计数归零时释放内存。
shared_ptr 需要分配另一块内存,叫做控制块,用于存储引用计数。

std::make_share<类>() 包括2次内存分配,先做一次new Entity的分配,然后是shared_ptr的控制内存块的分配

当将一个shared_ptr赋值给另一个shared_ptr会增加引用计数,当把一个shared_ptr赋值给一个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
30
31
32
33
#include <iostream>
#include <memory> // 需要包含

class Entity {
public:
Entity() {
std::cout << "Created Entity!" << std::endl;
}

~Entity() {
std::cout << "Destroyed Entity!" << std::endl;
}
};

int main() {
{
std::shared_ptr<Entity> e0;
{
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
std::weak_ptr<Entity> w = sharedEntity;
e0 = sharedEntity;
} // e0尚在作用域,引用为1
} // 引用归零,执行析构函数

{
std::weak_ptr<Entity> w;
{
std::shared_ptr<Entity> shareEntity = std::make_shared<Entity>();
w = shareEntity;
} // 引用归0,执行析构函数
}
std::cin.get();
}

weak_ptr

可被复制,但是不会增加额外的控制块来控制计数,仅仅声明这个指针还活着。

44 Copying and Copy Constructors in 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
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>

class String {
private:
char* m_buffer;
unsigned int m_size;

public:
String(const char* string) {
m_size = strlen(string);
m_buffer = new char[m_size + 1]; // 包含结束字符
// strcpy_s(m_buffer, m_size + 1, string); // 自动会复制结束标志
memcpy(m_buffer,string, m_size);
m_buffer[m_size] = 0;
}

~String() {
std::cout << "释放m_buffer" << std::endl;
delete[] m_buffer;
}

char& operator[](const unsigned int index) {
return m_buffer[index];
}

// 友元声明,让类外函数可以访问string
friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string) {
stream << string.m_buffer;
return stream;
}

int main() {
String s1 = "JKA";
String s2 = s1;
s2[2] = 'B';

std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
}

由于s1和s2的m_buffer指向同一缓冲区,超出作用域时两变量先后执行析构函数,会造成double free的问题。

C++默认拷贝函数,如果不重构就隐式调用如下函数(合成的拷贝构造函数):

1
2
3
4
5
6
7
String(const String& other)  // 类型是 "const 类 &"
: m_Buffer(other.m_Buffer), m_Size(other.m_Size) {}

// 或者
String (const String& other) {
memcpy(this, &other, sizeof(String));
}

设计的系统调用:

1
2
errno_t strcpy_s( char *restrict dest, rsize_t destsz, const char *restrict src ); 
void *memcpy(void *dest, const void *src, size_t n);

深复制:这里的方法是 拷贝构造函数,当复制第二个字符串时会被调用。

使用深拷贝后的代码:

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
#include <iostream>
#include <string>

class String {
private:
char* m_buffer;
unsigned int m_size;

public:
String(const char* string) {
m_size = strlen(string);
m_buffer = new char[m_size + 1]; // 包含终止字符
strcpy_s(m_buffer, m_size + 1, string); // 自动复制中指标志
//memcpy(m_buffer, string, m_size);
//m_buffer[m_size] = 0;
}

String(const String& other) // 拷贝构造函数,注意参数
: m_size(other.m_size) {
std::cout << "Copy" << std::endl;
m_buffer = new char[m_size + 1];
memcpy(m_buffer, other.m_buffer, m_size + 1);
}

~String() {
delete[] m_buffer;
}

char& operator[](const unsigned int index) {
return m_buffer[index];
}

// 友元声明
friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string) {
stream << string.m_buffer;
return stream;
}

void PrintString(String string) {
std::cout << string << std::endl;
}

int main() {
String s1 = "JKA";
String s2 = s1;
s2[2] = 'B';

PrintString(s1);
PrintString(s2);
}

// Out
Copy
Copy
JKA
Copy
JKB

问题的原因是没有PrintString中未使用引用,而是由进行了复制。

💡 技巧:尽可能通过const引用去传递参数。

45 The Arrow Operator in C++

->(*pointer).member 的语法糖。

const指针只能调用const方法。

一种用箭头计算偏移量的方法:

1
2
3
4
5
6
7
8
9
struct Vector3 {
float x, y, z;
};

int main() {
int offset = (int)&((Vector3*)nullptr)->y; // 取y成员的地址
std::cout << offset << std::endl;
std::cin.get();
}

46 Dynamic Arrays in C++ (std::vector)

标准模板库(Standard Template Library, STL)本质上是一个库,里面装满了容器、容器类型,这些容器包含特定的数据。整个库模板化,这意味着容器的底层数据类型由程序员决定。

标准模版库的速度不是最优先考虑的,需要同时兼顾兼容性和性能,很多情况下工作室和团队最终会创建自己的容器库。

vector其实更应当被称为ArrayList。原理:当超出空间时,会在内存中创建一个比第一个大的新数组,然后把数据都复制到新数组,然后删除旧的。
vector倾向于经常复制,所以并不能获得最佳性能。

存储vertex对象比存储指针在技术上更优(实际上很难说,要视情况而定),因为动态数组是内存连续的数组,它们都在同一条高速缓存线(cache line)上。但存放指针的话,重新分配内存时,数据不用复制只用复制指针。

遍历时尽量使用 for (Vertex& v : vertices) ,加不加const关系不大,但加&可有效避免复制。

删除第2个元素:vertices.erase(vertices.begin() +1) ,需要传入迭代器,而非序号。

47 Optimizing the usage of std::vector in 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
#include <iostream>
#include <vector>

class Vertex {
public:
int x, y, z;
Vertex(int x, int y, int z)
: x(x), y(y), z(z) {}

Vertex(const Vertex& vertex) // 拷贝构造函数
: x(vertex.x), y(vertex.y), z(vertex.z) {
std::cout << "Copied!" << std::endl;
}
};

int main() {
std::vector<Vertex> vertices;
vertices.push_back(Vertex{ 1, 2, 3 });
vertices.push_back({ 4, 5, 6 }); // 隐式转换,同上
vertices.push_back({ 7, 8, 9 });
std::cin.get();
}

// Out
Copied!
Copied!
Copied!
Copied!
Copied!
Copied!

包含6次copy,3次在main栈帧中创建Vertex局部变量后作为参数copy给push_back参数。第2次push_back时需要扩容,复制第一个Vertex,第3次push_back时也需要扩容,复制第一个和第二个Vertex。

优化:

  • reserve 提前申请内存,避免动态申请开销
  • emplace_back 直接在容器尾部创建元素,省略拷贝或移动过程
1
2
3
4
5
6
7
8
9
10
11
int main() {
std::vector<Vertex> vertices;
vertices.reserve(3);
vertices.emplace_back(Vertex{ 1, 2, 3 }); // 1次copy, 不要这样写
vertices.emplace_back(4, 5, 6); // 无{}喔
vertices.emplace_back(7, 8, 9);
std::cin.get();
}

// Out
Copied!

注意:vertices.emplace_back(4, 5, 6); // 无{}喔

48 Local Static in C++

local static:可以在局部作用域中使用static来声明一个变量。考虑变量的生存期和作用域。

生存期——在被删除前,在内存中会存在多久。
作用域——我们可以访问变量的范围。

函数内的局部静态和类成员的局部静态没有多少不同,除了类静态成员变量可以被类的不同对象访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Func() {
static int i = 0; // static 延长了变量的生存期
std::cout << i++ << std::endl;
}

int main() {
Func();
Func();
Func();
std::cin.get();
}

// Out
0
1
2

可能需要在程序的某处调用一个静态初始化函数来创建所有对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton {
public:
static Singleton& Get() {
static Singleton* s_Instance; // static 延长生存期至永远

static int i = 10;
++i;
std::cout << i << std::endl;
return *s_Instance;
}

void Hello() {
std::cout << "hello" << std::endl;
}
};

int main() {
Singleton::Get().Hello();
Singleton::Get().Hello();
}

本人对程序编译链接过程理解不深,等以后再完善下面2节的内容。

49 Using Libraries in C++ (Static Linking)

C++中没有包管理器。

对于大多数严肃的项目,Cherno推荐从源代码构建——编译器和链接器会进行优化,而非链接到预构建的二进制文件。能帮助调试,如果想修改库稍微改变一点就行。

选择32位还是64位,不取决于OS,而取决于目标应用程序。

库通常包含两部分,include和library,include是一堆头文件lib目录里有那些预先构建的二进制文件——动态库和静态库(并不是所有库都提供2种)。静态链接在技术上更快,因为编译器和链接器实际上可执行链接时优化之类的——在链接时知道要链接的函数。而动态链接库被运行时的程序装载时,程序的部分将被补充完整。

glfw3dll.lib实际上包含了glfw3.dll中所有的函数、符号位置,所以可以在编译时链接它们。如果没有此文件,也能使用glfw3dll,但需要通过函数名来访问dll文件内的函数。
glfw3.lib 作为静态链接库比其他的大得多,如果不想编译时链接,就链接这个,这样在exe运行时就不需要glfw3.dll。

C++中对C函数进行声明:exteren “C” int g

50 Using Dynamic Libraries in C++

动态链接是在运行时进行链接,而静态链接是在编译时发生的。
相比之下由于静态链接中编译器和链接器完全知道静态链接时实际进入应用程序的代码,其允许更多的优化发生。

GLFW同时支持静态和动态链接,二者使用相同的头文件。

glfw3dll.lib基本上就是一堆指向dll文件的指针,这样就不用在运行时去检索所有东西的位置,因此需要链接glfw3dll.lib。

glfw3.dll需要和exe文件放在一起才能被访问。

C++创建一个动态链接库,编译后会生成两个可用的文件一个是lib文件一个是dll文件,那么这个lib文件是干嘛的呢?
在使用动态库的时候,往往提供两个文件:一个引入库lib和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。
1. Load-time Dynamic Linking 载入时动态链接
这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中,其主要目的是便于代码共享
2. Run-time Dynamic Linking 运行时动态链接
这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址