C++ 知识点记录(3)内存模型和名称空间(Cap9)

这篇博客详细介绍了C++中的内存模型和名称空间。内存模型部分涵盖头文件管理、存储持续性、作用域和链接性,强调了自动存储、静态存储的特点以及链接性对全局变量的影响。名称空间部分讨论了如何避免命名冲突,使用using声明和编译指令,以及名称空间的其他特性。博客还提供了一些实用示例,帮助理解这些概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 单独编译

(1)头文件

        1)头文件中常包含的内容:

  • 函数原型
  • 使用#define或const定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

        2)头文件的书写格式:

        如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,取决于编译器),如果没有找到再去标准位置查找。因此,在包含自己的头文件时,应使用双引号而不是尖括号。

        3)系统将程序组合起来的步骤:

        只需将源代码文件加入到项目中,而不用加入头文件,这是因为#include指令管理头文件。另外不要使用#include来包含源代码文件,这样做将导致多重声明。

        在UNIX系统中编译由多个文件组成的C++程序:

  1. 编译两个源代码文件的UNIX命令:CC file1.cpp file2.cpp
  2. 预处理器将包含的文件与源代码文件合并,生成临时文件
  3. 编译器创建每个源代码文件的目标代码文件
  4. 链接程序将目标代码文件、库代码和启动代码合并,生成可执行文件

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

        4)头文件管理:

        在同一个文件中只能将同一个头文件包含一次,但可能使用包含了另一个头文件的头文件,这种规则就会被破坏。C/C++提供了一种标准技术来避免多次包含同一个头文件,即基于预处理器编译指令#ifndef(即if not defined)。

#ifndef COORDIN_H_
// ...
#endif
// 意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句。

        这种方式并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。大多数标准C和C++头文件都使用这种防护方案。

(2)单独编译示例:

头文件coordin.h:

// coordin.h -- 结构模板和函数原型
// 结构模板
#ifndef COORDIN_H_
#define COORDIN_H_

struct polar {
	double distance;  // 距离坐标
	double angle;     // 角度坐标
};
struct rect {
	double x;
	double y;         //直角坐标系
};

// 函数原型
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif // !COORDIN_H_

源文件:

// file1.cpp -- 3个文件的示例
#include <iostream>
#include "coordin.h"
using namespace std;
int main() {
	rect rplace;
	polar pplace;

	cout << "Enter the x and y values: ";
	while (cin >> rplace.x >> rplace.y) {
		pplace = rect_to_polar(rplace);
		show_polar(pplace);
		cout << "Next two numbers (q to quit): ";
	}
	cout << "Bye!\n";
	return 0;
}
// file2.cpp -- 包含file1.cpp的函数定义
#include <iostream>
#include <cmath>
#include "coordin.h"

// 将直角坐标转换为极坐标
polar rect_to_polar(rect xypos) {
	using namespace std;
	polar answer;
	answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
	answer.angle = atan2(xypos.y, xypos.x);
	return answer;
}

// 显示极坐标,将弧度表示的角度转换为角度
void show_polar(polar dapos) {
	using namespace std;
	const double Rad_to_deg = 57.29577951;
	cout << "distance = " << dapos.distance;
	cout << ", angle = " << dapos.angle * Rad_to_deg;
	cout << " degrees.\n";
}

        C++标准使用术语翻译单元(translation unit),而不是文件,进行单独编译,文件并不是计算机组织信息时的唯一方式。

        多个库的链接:由于C++允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此由不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果由源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。

2. 存储持续性、作用域和链接性

(1)C++四种存储数据的方案(存储持续性):

(区别在于数据保留在内存中的时间)

  • 自动存储持续性:在函数定义中声明的变量(包括函参),其存储持续性为自动。它们在程序开始执行其所属的函数或代码块时被创建,在执行完毕后其内存被释放。C++有2种存储持续性为自动的变量。
  • 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。
  • 线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可以同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
  • 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或者程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。

(2)作用域和链接

        1)作用域(scope):描述名称在文件(翻译单元)的多大范围内可见。

  • 局部:只在定义它的代码块中可用。
  • 全局(文件作用域):在定义位置到文件结尾之间都可用。
  • 自动变量作用域为局部
  • 静态变量的作用域取决于它是如何被定义的
  • 函数原型作用域:使用的名称只在包含参数列表的括号内可用,因此这些名称是什么以及是否出现都不重要
  • 类中声明的成员:作用域为整个类
  • 在名称空间中声明的变量:作用域为整个名称空间(全局作用域是名称空间作用域的特例)
  • 函数的作用域:整个类或整个名称空间,但不能是局部

        2)链接性(linkage):描述名称如何在不同单元间共享。

  • 外部:可在文件间共享
  • 内部:只能由一个文件中的函数共享

(自动变量的名称没有链接性,因为它们不能共享)

(3)自动存储持续性

        在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部没有链接性

        寄存器变量,关键字register

(4)静态持续变量

        C/C++为静态存储持续性变量提供3种链接性:

  • 外部链接性:可在其他文件中访问,创建时必须在代码块的外面声明它;
  • 内部链接性:只能在当前文件中访问,创建时必须在代码块的外面声明它,并且使用static限定符;
  • 无链接性:只能在当前函数或代码块中访问,创建时必须在代码块内声明它,并使用static限定符。

        编译器将分配固定内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。

#include <iostream>
using namespace std;

int global = 1000;             //静态持续性;全局作用域,从定义位置到文件结尾都可使用;外部链接性,可在其他文件中访问
static int one_file = 50;      //静态持续性,全局作用域,内部链接性,只能在当前文件中访问
void funct1(int n);

int main() {
	

    return 0;
}

void funct1(int n) {
	static int count = 0;      //静态持续性,作用域为局部,无链接性
	int llama = 0;
}

        下表总结了引入名称空间之前使用的存储特性,其列出了关键字static的两种用法,但含义不同:用于局部声明,以指出变量是无链接性的静态变量时,static表示的是存储持续性;而用于代码块外的声明时static表示内部链接性,而变量已经是静态持续性了。

5种变量存储方式
存储描述持续性作用域链接性如何声明
自动自动代码块在代码块中
寄存器自动代码块在代码块中,使用关键字register
静态,无链接性静态代码块在代码块中,使用关键字static
静态,外部链接性静态文件外部不在任何函数内
静态,内部链接性静态文件内部不在任何函数内,使用关键字static

        静态变量的初始化

        如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和静态结构将每个元素和成员的所有位都设置为0。即未被初始化的静态变量所有位都被设置为0,这种变量被称为零初始化的(zero-initialized)。

        静态初始化:零初始化+常量表达式初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。

        动态初始化:变量将在编译后初始化。

(5)静态持续性、外部链接性

        链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件,外部变量又称为全局变量。

        1)单定义规则

        一方面在每个使用外部变量的文件中,都必须声明它。另一方面,C++的“单定以规则”要求变量只能有一次定义。C++提供了两种声明,一种是定义声明,一种是引用声明,引用声明使用关键字extern且不进行初始化

        如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。

        注意,单定义规则不意味着不能有多个变量同名,局部变量可能隐藏同名的全局变量。

// 9.5 external.cpp -- external variables
// compile with suport.cpp
#include <iostream>
using namespace std;
// 外部变量
double warming = 0.3;
// 函数原型
void update(double dt);
void local();

int main() {
	cout << "Global warming is " << warming << " degrees.\n";
	update(0.1);
	cout << "Global warming is " << warming << " degrees.\n";
	local();
	cout << "Global warming is " << warming << " degrees.\n";

    return 0;
}
#include <iostream>
extern double warming;
void update(double dt);
void local();

using std::cout;
void update(double dt) {
	extern double warming;    // 意识是通过这个名称使用外部变量warming,由于update()中没有定义同名自动变量,因此该声明可选
	warming += dt;
	cout << "Updating global warming to " << warming;
	cout << " degrees.\n";
}

void local() {
	double warming = 0.8;     // 局部变量隐藏全局变量
	cout << "Local warming = " << warming << " degrees.\n";
	cout << "But global warming = " << ::warming;   // 作用域解析运算符(::)放在变量名称前表示使用变量的全局版本,用这个更明确更安全
	cout << " degrees.\n";
}

上述程序示例关键点:作用域解析运算符(::)、局部变量隐藏全局变量

        2)何时适合使用全局变量

        计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下应该使用局部变量,在需要知晓时才传递数据。

        全局变量尤其适用于表示常量数据,搭配const来防止数据被修改:

const char* const months[12] = {
    "January", "February", "March", "Apirl", "May",
    "June", "July", "Augest", "September", "October",
    "November", "December"
}; 

(6)静态持续性、内部链接性

        将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量

(7)静态存储持续性、无链接性

        将static限定符用于在代码块中定义的变量,将导致该局部变量的存储持续性变为静态。因此,在两次函数调用之间,静态变量的值将保持不变。(静态变量适用于再生——可以用它们将瑞士银行的密码账号传递到下一个要去的地方)。另外,如果初始化了静态变量,则程序只在启动时进行一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。

        利用静态自动变量读取总字数:

// static.cpp -- using a static local variable
#include <iostream>
// 常量
const int ArSize = 10;
// 函数原型
void strcount(const char* str);

int main() {
	using namespace std;
	char input[ArSize];
	char next;

	cout << "Enter a line:\n";
	cin.get(input, ArSize);
	while (cin) {
		cin.get(next);
		// 处理输入可能长于目标数组的方法,方法cin.get(input, ArSize)将一直读取输入,直到到达行尾或者读取了ArSize-1为止。
		// 它把换行符留在输入队列里
		while (next != '\n')   
			cin.get(next);  //丢弃余下的字符
		strcount(input);
		cout << "Enter next line (empty line to quit):\n";
		cin.get(input, ArSize);   // !!!!!试图使用get(char*, int)读取空行将导致cin为false
	}
	cout << "Bye\n";
    return 0;
}

void strcount(const char* str) {
	using namespace std;
	static int total = 0;
	int count = 0;

	cout << "\"" << str << "\" contains ";
	while (*str++)
		count++;
	total += count;
	cout << count << " characters\n";
	cout << total << " characters total\n";
}

(8)说明符和限定符

        有些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。

存储说明符

  • auto:指出变量为自动变量,但在C++11中,用于自动类型推断
  • register:用于在声明中指示寄存器存储,C++11中只是显示地指出变量是自动的
  • static
  • extern:表明是引用声明,即声明引用在其他地方定义的变量
  • thread local:指出变量的持续性与其所属的线程的持续性相同
  • mutable

        1)cv-限定符

  • const
  • volatile

        关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。可能是硬件修改或者两个程序互相影响共享数据。该关键字的作用是为了改善编译器的优化能力。

        2)mutable

        mutable指出,即使结构(或类)变量为const,其某个成员也可以被修改。

struct data {
	char name[30];
	mutable int accesses;
};

int main() {
	const data veep = { "Claybourne Clodde", 0 };
	strcpy(veep.name, "Joye Joux");   // 不允许
	veep.accesses++;                  // 允许

	return 0;
}

        3)const

        在C++中,const限定符对默认存储类型稍有影响。在默认情况下,全局变量的链接性为外部,而const全局变量的链接性为内部。C++修改了常量类型的规则,让程序员更轻松。例如,假如将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。

        如果出于某种原因,程序员希望某个常量的链接性为外部,则可以使用extern关键字来覆盖默认的内部链接性。

extern const int states = 90;

(9)函数和链接性

  1. 所有函数的存储持续性都自动为静态的
  2. 默认情况下,函数的链接性为外部,即可以在文件间共享(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)
  3. 可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字
  4. 同变量一样,静态函数可以覆盖外部定义
  5. 单定义规则使用于非内联函数
  6. 内联函数不受单定义规则的约束,则允许程序员将内联函数放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同
  7. C++在哪里查找函数?如果函数是静态的,则只在该文件中查找函数定义;否则编译器(包括链接程序)将在所有程序文件中查找;如果在程序文件中没找到,则去查找库函数。(因此如果定义和库函数同名的函数,编译器优先使用程序员定义的版本)。

(10)语言链接性

C语言链接性、C++语言链接性(名称修饰 Cap8)

        链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同。如果要在C++程序中使用C库中预编译的函数,将出现什么情况?

        为解决这个问题,可以用函数原型来指出要使用的约定:

extern "C" void spiff(int);
extern void spiff(int);
extern "C++" void spiff(int);

        C和C++链接性是C++标准指定的说明符。

(11)存储方案和动态分配

        使用C++运算符new(或C函数malloc)分配的内存被称为动态内存,动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。

        通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,一块用于动态存储。

1)使用new运算符初始化

int *pi = new int (6);   // C++98
double *pd = new double(99.99);    //C++98

struct where { double x; double y; double z; };
where* one  = new where { 2.5, 5.3, 7.2 };   //C++11
int* ar = new int[4] { 2, 4, 6, 7 };

int* pin = new int {6};  //C++11
double* pdo = new double {99.99};

2)new失败时

        将引发异常std::bad_alloc

3)new:运算符、函数和替换函数

运算符newnew[]分别调用如下函数:

void* operator new(std::size_t);  // std::size_t是一个typedef,对应于合适的整型

void* operator new[](std::size_t);    

这些函数被称为分配函数,位于全局名称空间中。

例:

int* pi = new int; 被转换为:int* pi = new (sizeof(int));

运算符deletedelete[]分别调用如下函数:

void operator delete(void* );  

void operator delete[](void* );

这些函数被称为释放函数,位于全局名称空间中。

4)定位new运算符

        通常,new运算符负责在堆(heap)中找到一个足以能够满足要求的内存块。其另一种变体,定位new运算符,让程序员能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理章程处理需要通过特定地址进行访问的硬件在特定位置创建对象。 

        要使用定位new特性,首先要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同,具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。

new运算符的4种用法

// 此示例用两个静态数组来为定位new运算符提供内存空间
#include <new>
struct chaff {
	char dross[20];
	int slag;
};
char buffer1[50];
char buffer2[500];
int main() {
	chaff* p1, * p2;
	int* p3, * p4;
	// new运算符的传统形式
	p1 = new chaff;            // 在堆中分配可以存储chaff变量的内存
	p3 = new int[20];          // 在堆中分配可以存储int数组的内存
	// 定位new运算符的2种形式
	p2 = new (buffer1) chaff;  // 将结构放在buffer1中
	p4 = new (buffer2) int[20];// 将int数组放在buffer2中
}

        程序示例:

// newplace.cpp -- using placement new
#include <iostream>
#include <new>
const int BUF = 512;
const int N = 5;
char buffer[BUF];       // 内存块

int main() {
	using namespace std;
	double* pd1, * pd2;
	int i;
	cout << "Calling new and placement new: \n";
	pd1 = new double[N];    // 堆
	pd2 = new (buffer) double[N];    // buffer数组
	for (i = 0; i < N; i++)
		pd2[i] = pd1[i] = 1000 + 20.0 * i;
	cout << "Memory address:\n" << " heap: " << pd1
		<< " static: " << (void*)buffer << endl;   // 如果不进行强制转换,cout将显示一个字符串
	cout << "Memory contents:\n";
	for (i = 0; i < N; i++) {
		cout << pd1[i] << " at " << &pd1[i] << "; ";
		cout << pd2[i] << " at " << &pd2[i] << endl;
	}

	cout << "\nCalling new and placement new a second time:\n";
	double* pd3, * pd4;
	pd3 = new double[N];              // 找新的内存块
	pd4 = new (buffer) double[N];     // 在旧地址重写
	for (i = 0; i < N; i++)
		pd4[i] = pd3[i] = 1000 + 40.0 * i;
	cout << "Memory contents:\n";
	for (i = 0; i < N; i++) {
		cout << pd3[i] << " at " << &pd3[i] << "; ";
		cout << pd4[i] << " at " << &pd4[i] << endl;
	}

	cout << "\nCalling new and placement new a third time:\n";
	delete[] pd1;
	pd1 = new double[N];
	pd2 = new (buffer + N * sizeof(double)) double[N];
	for (i = 0; i < N; i++)
		pd2[i] = pd1[i] = 1000 + 60.0 * i;
	cout << "Memory contents:\n";
	for (i = 0; i < N; i++) {
		cout << pd1[i] << " at " << &pd1[i] << "; ";
		cout << pd2[i] << " at " << &pd2[i] << endl;
	}
	delete[] pd1;
	delete[] pd3;
	// buffer指定的是内存是静态内存,而delete只能用于这样的指针:指向常规new运算符分配的堆内存
	return 0;
}

        注意:detele只能用于这样的指针:指向常规new运算符分配的堆内存。

3. 名称空间

(1)传统的C++名称空间

术语:声明区域(declaration region),潜在作用域(potential scope),作用域(scope)。

(2)新的名称空间特性

        C++提供这样一种功能:通过定义一种新的声明区域来创建命名的名称空间。

namespace Jack{
    double pail;
    void fetch();
    int pal;
    struct Well { ... };
}

namespace Jill{
    double bucket(double n) { ... }
    double fetch;
    int pal;
    struct Hill { ... };
}

        名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量 ???)。

        访问给定名称空间中的名称,最简单的方法是通过作用域解析符::,使用名称空间来限定该名称。

        1)using声明和using编译指令

using声明:using Jill :: fetch;

using编译指令:using namespace Jill;

注意:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。using声明的作用域与其所在的声明区域相同。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。一般情况下,使用using声明比使用using编译指令更安全。 

        2)名称空间的其他特性

        可以用嵌套名称空间来创建一个包含常用using声明的名称空间。

        未命名的名称空间提供了链接性为内部的静态变量的替代品。

        3)名称空间示例

// namesp.h
#include <string>
// 建立pers和debts名称空间
namespace pers {
	struct Person {
		std::string fname;
		std::string lname;
	};
	void getPerson(Person&);
	void showPerson(const Person&);
}

namespace debts {
	using namespace pers;  // using 编译指令
	struct Debt {
		Person name;
		double amount;
	};
	void getDebt(Debt&);
	void showDebt(const Debt&);
	double sumDebts(const Debt ar[], int n);
}
// namesp.cpp -- namespaces
#include <iostream>
#include "namesp.h"

namespace pers {
	using std::cout;
	using std::cin;
	void getPerson(Person& rp) {
		cout << "Enter first name: ";
		cin >> rp.fname;
		cout << "Enter last name: ";
		cin >> rp.lname;
	}
	void showPerson(const Person& rp) {
		std::cout << rp.lname << ", " << rp.fname;
	}
}

namespace debts {
	void getDebt(Debt& rd) {
		getPerson(rd.name);
		std::cout << "Enter debt: ";
		std::cin >> rd.amount;
	}
	void showDebt(const Debt& rd) {
		showPerson(rd.name);
		std::cout << ": $" << rd.amount << std::endl;
	}
	double sumDebts(const Debt ar[], int n) {
		double total = 0;
		for (int i = 0; i < n; i++)
			total += ar[i].amount;
		return total;
	}
}
// usenamesp.cpp -- using namespaces
#include <iostream>
#include "namesp.h"
void other(void);
void another(void);

int main() {
	using debts::Debt;
	using debts::showDebt;   // 如果函数被重载,则一个using声明将导入所有版本
	Debt golf = { {"Benny", "Goatsniff"}, 120.0 };
	showDebt(golf);
	other();
	another();
	return 0;
}

void other(void) {
	using std::cout;
	using std::endl;
	using namespace debts;   // 不太好的方法
	Person dg = { "Doodles", "Glister" };
	showPerson(dg);
	cout << endl;
	Debt zippy[3];
	int i;
	for (i = 0; i < 3; i++) 
		getDebt(zippy[i]);
	for (i = 0; i < 3; i++)
		showDebt(zippy[i]);
	cout << "Total debt: $" << sumDebts(zippy, 3) << endl;
	return;
}

void another(void) {
	using pers::Person;
	Person collector = { "Milo", "Rightshift" };
	pers::showPerson(collector);
	std::cout << std::endl;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值