以下为个人学习笔记整理。参考书籍《C++ Primer Plus》

# 内存模型和名称空间

# 单独编译

将程序进行合理的拆分,可以降低代码的维护成本:

  • 头文件:包含结构体声明和使用这些结构体的函数原型。
    • 函数原型。(函数定义不要放在头文件,如果同一个程序的多个源文件使用了函数定义,会导致出错)
    • 使用 #defineconst 定义的符号常量。
    • 结构体声明。
    • 类声明。
    • 模板声明。
    • 内联函数。
    • 名称空间内上述内容的定义。
  • 源代码文件:包含与结构体有关的函数的代码,函数的定义,名称空间内函数的定义。
  • 源代码文件:包含调用与结构体相关函数的代码,调用名称空间内函数的代码。

# include 操作

include <>include "" 。功能相同,但是查找文件的顺序不同。通常引入自己的头文件时,应该用「双引号」。

  • 尖括号括起来的文件名,编译器首先会在存储标准头文件的主机系统的文件系统查找。
  • 双引号括起来的文件名,编译器会先查找当前的工作目录或源代码目录,没有找到才回去文件系统查询。

在 IDE 中,不要将头文件加入到项目列表中,也不要在源代码文件中使用 #include 来包含其他源代码文件

image-20210302144543003

# 头文件管理

同一个文件中,只能对同一个头文件包含一次。生病后吃药是个不错的选择,但能够防范于未然自然更好 —— ifndefendif

#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)。

image-20210302160527157

# 自动存储

# 自动变量的初始化

可以使用任何在声明时,其值为已知的表达式来初始化自动变量。

int w; //w 的值是不确定的
int x = 5;
int f = MAX - 1;
int y = 2 * x;

# 自动变量的管理

通常情况下,自动变量会被存储在「栈」中。之所以称之为「栈」,是因为新数据被放在了和旧数据相邻的内存单元中。并且有着先进后出(LIFO)的性质。

image-20210302155022697

# 寄存器变量

寄存器变量通过关键字 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;
}

image-20210302153505661

# 链接方式

  • 外部链接性:可在其他文件中访问。
  • 内部链接性:只能在当前文件内访问。
  • 无链接性:只能在当前函数或者代码块中访问。

创建方式如下:

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 修饰。修饰变量不会被初始化。

image-20210303095249721

在多文件程序中,可以且只能在一个文件中定义一个外部变量,使用该变量的其他文件必须用 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:运算符、函数和替换函数。 newnew[]deletedelete[] 分别调用如下函数:

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)

通过文件。名称空间,代码块等划分出的一片片区域。

image-20210303144700972

# 潜在作用域(potential scope) & 作用域(scope)

  • 潜在作用域:变量的潜在作用域从声明位置开始,一直到声明区域结束。因此潜在作用域比声明区域小,只有被定义了,才能够使用。

  • 作用域:变量对于程序「可见」的范围被称作作用域。

image-20210303144823918

# 新的名称空间 ——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 声明」尽量定义为局部而非全局。