以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# 内存模型和名称空间
# 单独编译
将程序进行合理的拆分,可以降低代码的维护成本:
- 头文件:包含结构体声明和使用这些结构体的函数原型。
- 函数原型。(函数定义不要放在头文件,如果同一个程序的多个源文件使用了函数定义,会导致出错)
- 使用
#define
或const
定义的符号常量。 - 结构体声明。
- 类声明。
- 模板声明。
- 内联函数。
- 名称空间内上述内容的定义。
- 源代码文件:包含与结构体有关的函数的代码,函数的定义,名称空间内函数的定义。
- 源代码文件:包含调用与结构体相关函数的代码,调用名称空间内函数的代码。
# include 操作
include <>
和 include ""
。功能相同,但是查找文件的顺序不同。通常引入自己的头文件时,应该用「双引号」。
- 尖括号括起来的文件名,编译器首先会在存储标准头文件的主机系统的文件系统查找。
- 双引号括起来的文件名,编译器会先查找当前的工作目录或源代码目录,没有找到才回去文件系统查询。
在 IDE 中,不要将头文件加入到项目列表中,也不要在源代码文件中使用
#include
来包含其他源代码文件
# 头文件管理
同一个文件中,只能对同一个头文件包含一次。生病后吃药是个不错的选择,但能够防范于未然自然更好 —— ifndef
和 endif
。
#ifndef XXX_H // if not def XXX_H | |
#include "xxx.h" | |
#endif // !XXX_H |
或者在头文件中加入以下代码片段:
#pragma once |
# 题外话 —— 多个库的链接
C++ 允许编程人员自定义编译器的名称修饰(根据函数名称生成一些内部约定的变量名,例如 func -> _func
)。为此如果两个不同的编译器生成同一个函数的名称修饰时,往往不一致。这会导致编译器生成的函数调用和函数定义不匹配。所以,通常情况下,请确保所有的对象文件和库都是由「同一个编译器」生成的。
# 存储持续性、作用域和链接性
存储持续性的分类:
- 自动:函数内定义的临时变量的存储性为自动的,在程序开始时创建,结束后销毁。C++ 有两种存储持续性为自动的变量:
- 自动变量
- 寄存器变量
- 静态:在函数以外定义的变量(全局变量)或者被
static
修饰的变量(静态变量)为静态的。他们的生命周期伴随整个程序。 C++ 有三种存储持续性为静态的变量。- 全局静态变量
- 内部静态变量
- 局部静态变量
- 线程(C++11):如果变量使用关键字
thread_local
声明,则其生命周期与所属线程一样。 - 动态:用
new
运算符分配的内存将一直存在,直到delete
运算符将其释放,或者程序结束。又被称为「自由存储」(free store)或「堆」(heap)。
# 自动存储
# 自动变量的初始化
可以使用任何在声明时,其值为已知的表达式来初始化自动变量。
int w; //w 的值是不确定的 | |
int x = 5; | |
int f = MAX - 1; | |
int y = 2 * x; |
# 自动变量的管理
通常情况下,自动变量会被存储在「栈」中。之所以称之为「栈」,是因为新数据被放在了和旧数据相邻的内存单元中。并且有着先进后出(LIFO)的性质。
# 寄存器变量
寄存器变量通过关键字 register
修饰。用于显式的指出变量是自动变量。
# 静态持续变量
静态变量在整个程序的生命周期内数目是确定的,所需不需要单独分配像「栈」进行管理。另外,静态变量的默认值是 0,像数组或是结构体初始化时,会把所有的成员都初始化为 0。
对于自动数组和结构,有些编译器不支持初始化值,但有一些编译器却可以。
# 静态变量的初始化
C++11 新增了关键字 constexpr
,提供了创建常量表达式的方式,有兴趣再深入了解。
#include<cmath> | |
int x; // 初始化为 0 | |
int y = 2 * x; // 常量表达式初始化 | |
int z = 13 * 13; // 常量表达式初始化 | |
int enough = 2 * sizeof(long) + 1; // 常量表达式初始化 | |
const double pi = 4.0 * atan(1.0); // 动态初始化 |
# 作用域和链接
如下代码块中花括号内的 teledeli
就是一个局部变量,虽然在括号外也定义了一个相同名称的变量,但实际上在花括号内,外部的同名变量是被隐藏的。
int main(){ | |
int teledeli = 1; | |
{ | |
int teledeli = 100; | |
cout << teledeli << endl; | |
} | |
cout << teledeli << endl; | |
} |
# 链接方式
- 外部链接性:可在其他文件中访问。
- 内部链接性:只能在当前文件内访问。
- 无链接性:只能在当前函数或者代码块中访问。
创建方式如下:
int gl_var = 1; // 外部链接性 | |
static int st_var = 10; // 内部链接性 | |
void func(){ | |
static int f_st_var = 100; // 无链接性 | |
} |
# 静态持续性、外部链接性
链接性为外部的变量被称为外部变量,外部变量为静态且作用域在整个文件
如果在函数内想要访问外部变量可以通过如下两种方法:
double global_var = 1.1; | |
// #1 | |
void func(){ | |
extern double global_var; | |
cout << global_var << endl; | |
} | |
// #2 | |
extern double global_var; | |
void func(){ | |
double global_var = 2.2; // is local | |
cout << global_var << endl; // 2.2 | |
cout << ::global_var << endl; // is global = 1.1 | |
} |
# 单定义规则:
使用外部变量前必须且只能声明一次。
- 定义声明(defining declaration),简称定义。定义会给变量分配存储空间。
- 引用声明(referencing declaration),简称声明。声明不会给变量分配存储空间。声明需要用关键字
extern
修饰。修饰变量不会被初始化。
在多文件程序中,可以且只能在一个文件中定义一个外部变量,使用该变量的其他文件必须用
extern
关键字声明。
# 静态持续性、内部链接性
用 static
限定符修饰的变量作用域为当前整个文件,不可在其他文件内使用,链接性为内部。
- 如果同时定了了同名的「静态内部变量」和「静态外部变量」,那么静态外部变量将会被隐藏,类似函数中的情况。
# 静态存储持续性、无链接性
「静态局部变量」只会在代码块内初始化一次。换句话说,如果一个函数内声明了一个「静态局部变量」,那么它的值会被下一次调用时所复用。不会跟随函数结束而销毁,仅仅是失去了活跃性而已。
# 函数和链接性
C++ 不允许在函数内声明函数,所以函数都是静态的。默认情况下函数都是外部的,但可以通过 static
修饰为内部,使用时必须在函数原型和定义都同时使用。
static int func(); | |
static int func(){ | |
//... | |
} |
除了内联函数以外,其他函数也满足单定义规则。
# C++ 如何查找函数?
- 如果函数原型是
static
的,那么只会在当前文件进行查找。 - 如果函数原型是非
static
的,则会在所有源文件内查找,如果出现一个以上的定义是会报错。如果没有查询到结果,则会搜索「标准库」。 - 如果「标准库」存在则调用,如果程序定义了和「标准库」相同名字的函数,那么其将会被优先执行,但是 C++ 并不推荐这么使用。
# 语言链接性
C++ 中一个函数名称的不同参数定义可以对应多个函数。为了对其进行区分,C++ 通过编译时进行「名称矫正」或「名称修饰」。
C++ 提供了控制使用那种方式来查找链接:
extern "C" void spiff(int); // 使用 c 格式进行修饰 | |
extern "C" void spiff(int); // 使用 C++ 格式进行修饰(默认) | |
extern "C++" void spiff(int); // 使用 C++ 格式进行修饰 |
# 名称修饰
会根据函数名,返回值和参数,通过名称修饰转变为其他格式的函数名。
例如:
- 在 C++ 下:
spiff(int)
会被修饰为_spiff_i
;而spiff(double, double)
会被修饰为_spiff_d_d
。 - 在 C 下:
spiff(int)
会被修饰为_spiff
;
# 存储方案和动态分配
C++ 通过 new
或者 malloc()
分配的内存,被称为动态内存。
与自动内存(栈)不同,动态内存不是 LIFO 的。
通常编译器使用三块独立的内存:
- 一块用于静态变量。
- 一块用于自动变量。
- 一块用于动态存储。
# new 运算符的那些事
- 使用 new 运算符初始化。
// C++ 98 | |
int* pi = new int(6); | |
// C++ 11 | |
int* pi = new int{6}; |
new 失败时。如果没有能够成功分配到内存,那么 C++ 将会引发异常
std:bad_alloc
。new:运算符、函数和替换函数。
new
和new[]
、delete
和delete[]
分别调用如下函数:
void* operator new(std::size_t); | |
void* operator new[](std::size_t); | |
void operator delete(void*); | |
void operator delete[](void*); | |
// example: | |
int* pi = new int; // same: int* pi = new(sizeof(int)); | |
int* pa = new int[40]; // same: int* pa = new(40 * sizeof(int)); | |
delete pi; // same: delete(pi); |
- 定位 new 运算符。通常情况下 new 运算符只能够分配「堆」上的内存。但有一种例外,被称为「定位 new」运算符。其作用是指定想要的内存位置。
char buff[1000]; // 全局变量,静态内存 | |
double* p = new(buff + 10 * sizeof(double)) double[10]; // 指定在静态内存上分配一块地址用于构建 double [10] 数组 | |
delete [] p; // 这里由于分配的内存是静态内存,所以不能够用 delete,而需要用 delete [] 来进行释放,尽管内存分配使用的是 new 而非 new [],因为 delete 只能管理「堆」上的内存,此外还需要注意 delete [] 只能回收分配在指定位置的内容,但不会调用对象的析构函数,如果对象构造函数中存在 new 分配的内存,还需要「显式的调用析构函数」,这点非常重要 p->~ClassName (); |
- 定位 new 运算符的其他形式。
int* p = new int; // new(sizeof(int)) | |
int* p = new(buffer) int; // new(sizeof(int), buffer) | |
int* p = new(buffer) int[10]; // new(10 * sizeof(int), buffer) |
# 说明符和限定符
# 存储说明符
- auto(
C++11
之后不再是说明符):C++11
之前用于声明自动变量。C++11
之后用于自动类型推断。 - register:声明指示寄存器存储。
- static:声明静态存储。
- extern:引用声明。
- thread_local(C++11 新增的说明符):声明变量持续性和线程相同。
- mutable:即使结构体被声明为
const
,只要被mutable
修饰的变量,依旧可以修改。
一次声明中最多只能有一个说明符( thread_local
除外)。
# 限定符
- const:限定变量不可修改。
- volatile:声明变量是可以被其他因素修改,例如操作系统等,声明后编译器将不会对该代码进行优化。
# const 的那些事📜
const 修饰的「静态全局变量」会被转会为「静态内部变量」。
原因也很简单:如果不进行转换,那么在其他多个源文件引用某个头文件定义的 const 静态全局变量时,将会出现重定义的错误。
const int global_var = 1; // same: static const int global_var = 1; |
如果希望 const 修饰的对象的链接性为外部。那么可以增加 extern 进行修饰。这样做会导致只能由一个源文件引用该变量的定义,否则会导致重定义问题。
extern const int global_var = 1; |
# 名称空间
为了避免各个项目中定义的变量、函数、结构、枚举、类等名称冲突,从而引入了名称空间的概念,对其加以区分。
# 传统 C++ 名称空间
# 声明区域(declaration region)
通过文件。名称空间,代码块等划分出的一片片区域。
# 潜在作用域(potential scope) & 作用域(scope)
潜在作用域:变量的潜在作用域从声明位置开始,一直到声明区域结束。因此潜在作用域比声明区域小,只有被定义了,才能够使用。
作用域:变量对于程序「可见」的范围被称作作用域。
# 新的名称空间 ——namespace
通过定义一个新的声明区域来创建命名的名称空间,用以区分不同项目之间的同名变量。
名称空间可以是全局的,定义在最外部。也可以是局部的,定义在其他名称空间内。
通常情况下名称空间的链接性为外部(除非名称空间内引用了 const
修饰的常量),但是使用的时候需要通过 include
链接使用。
namespace Program1{ | |
double d; | |
void f(); | |
int i; | |
struct st{}; | |
float f; | |
} | |
namespace Programe2{ | |
double d; | |
void f(); | |
int i; | |
struct st{}; | |
namespace Programe2_1{ | |
double d; | |
} | |
using namespace Program1; | |
} | |
namespace Programe3{ | |
const double d; | |
using Program1::f; | |
} | |
int main(){ | |
cout << Programe1::d << endl; // 通过::来进行访问 | |
cout << Programe2::Programe2_1::d << endl; // 名称空间支持嵌套 | |
} |
# using 声明和 using 编译指令
using 声明可以将特定的名称添加到所属的声明区域:
using Program1::d; | |
double d = 1.1; | |
cout << d << endl; // Program1::d | |
cout << ::d << endl;// gloval d |
using 编译可以把名称空间内所有名称都加入声明区域。当局部变量中存在相同变量时,名称空间内的将被隐藏。
using namespace Programe2; | |
// 两者效果一样 | |
cout << d << endl; // Programe2::d | |
cout << Programe2::d << endl; // Programe2::d | |
double d = 2.1; | |
cout << d << endl; // local d | |
cout << Programe2::d << endl; // Programe2::d |
推荐使用「using 声明」而非「using 编译」。前者可以在发现名称重复是抛出错误,而后者则会隐藏。相比于潜伏的病症,过早的暴露不失为一种好选择👍
# 名称空间的其他特性
# 编译传递
namespace elements{ | |
namespace fire{ | |
int flame; | |
... | |
} | |
float water; | |
} | |
namespace myth{ | |
using namespace elements; | |
} | |
using namspace myth; | |
// 等效于同时引入 myth 和 elements 两个名称空间 | |
//using namespace myth; | |
//using namespace elements; |
# 名称空间别名
有时候名称空间过长,对于编码往往是个负担。
namespace MEF = myth::elements::fire; | |
using MEF::flame; |
# 未命名的名称空间
未命名的名称空间和全局变量类似,但是不能够被其他文件 using
。
更像是静态内部变量的一种替代。两者的区别在于:
static
变量链接为内部。- 未命名名称空间的链接为外部,对于某些实例化需求必须为外部链接的对象两者表现不同(例如:模板)。
static int count; // 定义在某个文件作用域内的静态局部变量 | |
namespace{ // 定义在某个命名空间内的静态全局变量 | |
int count; | |
} |
# 命名空间指导原则
- 使用在「已命名的名称空间中声明的变量」代替「外部全局变量」。
- 使用在「已命名的名称空间中声明的变量」代替「静态全局变量」。
- 如果开发了一个函数库或类库,请将其放在名称空间内。
- 少用「using 编译指令」,尽量用「using 声明」。
- 不要在头文件中使用「using 编译指令」,这样会掩盖哪些名称可用;另外包含头文件的顺序也会决定变量的生效规则。
- 导入名称时,尽量使用作用域解析运算符(::)或「using 声明」。
- 「using 声明」尽量定义为局部而非全局。