Cpp复习 Chapter 2 声明vs定义、头文件、存储持续性作用域和链接性、名称空间


Cpp系列笔记目录

【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板
【Cpp筑基】二、声明 vs 定义、头文件、存储持续性作用域和链接性、名称空间
【Cpp筑基】三、对象和类
【Cpp筑基】四、重载运算符、友元、类的转换函数
【Cpp筑基】五、类的继承、虚函数、抽象基类


1. 声明vs定义

什么是声明?什么是定义?

在C++中,声明(Declaration)和定义(Definition)是两个重要的概念,它们在程序的组织和编译过程中起着不同的作用。声明是告诉编译器某个变量、函数、类或者其他标识符的名称及其类型,但不包含具体的实现或者初始化,声明的主要目的是让编译器知道这些标识符的存在及类型。

  • 声明(Declaration):告诉编译器某个变量的名字和类型,但不分配存储空间(除非它也是一个定义)。
  • 定义(Definition):不仅告诉编译器变量的名字和类型,还分配存储空间。

声明我们只需要告诉编译器变量的名字和类型即可,不需要为其分配存储空间,例如:

  1. 变量声明
1
extern int x;		// 声明一个变量
  1. 函数声明
1
int add(int a, int b);// 声明一个名为add的函数,接受两个整数参数并返回一个整数
  1. 类声明
1
class Myclass;	// 声明一个类MyClass,但不定义其成员

在定义中我们需要为变量分配存储空间,例如:

  1. 变量定义
1
2
3
int x;			// 定义一个变量,但是没有 为其赋值
int y = 10; // 定义一个变量,并且初始化
extern int z = 20; // 定义一个外部变量,并且它会初始化为20
  1. 函数定义
1
2
3
int add(int a, int b) { // 定义add函数
return a + b; // 实现具体的功能
}
  1. 类定义
1
2
3
4
5
6
class MyClass { // 定义类MyClass
public:
void display() {
// 方法的实现
}
};

2. 头文件

学过C语言的同学都知道头文件,在C++中,头文件(header files)用于声明程序中所需的函数、类、变量和其他标识符,而不进行实际的定义和实现。头文件通常以.h或者.hpp作为扩展名。它们的主要作用是支持代码的模块化和重用,并简化项目的管理和编译过程。头文件的主要内容通常有:

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

一个头文件的简单例子:

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
#ifndef MYHEADER_H
#define MYHEADER_H

// 使用 #define 定义的符号常量
#define PI 3.14159

// 函数原型
void printMessage(const char* message);
int add(int a, int b);

// 使用 const 定义的符号常量
const int MAX_SIZE = 100;

// 结构体声明
struct Point {
int x;
int y;
};

// 类声明
class MyClass {
public:
MyClass();
void display() const;
private:
int data;
};

// 模板声明
template<typename T>
T multiply(T a, T b);

// 内联函数
inline int square(int x) {
return x * x;
}

#endif // MYHEADER_H

头文件管理

在同一个文件中只能将同一个头文件包含一次,记住这个规则很容易,但是很可能在不知道的情况下将头文件进行了多次包含,例如,可能使用包含了另一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件,它是基于预编译指令#ifndef(即if not define的),下面的代码意味着仅当以前没有使用预处理器编译指令#define定义名称MYHEADER_H,才处理#ifndefendif之间的语句:

1
2
3
4
#ifndef MYHEADER_H
#define MYHEADER_H
... // place include file contents here
#endif // MYHEADER_H

另外,如果头文件包含在尖括号<>中,则Cpp编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号""中,编译器将首先查找当前的工作目录或者源代码目录,如果没有找到头文件,则将在标准位置进行查找。

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

3.1 存储持续性

Cpp中使用四种不同的方案来存储数据,这些方案的区别在于数据保留在内存中的时间,主要分为:

  • 自动存储持续性(默认):Cpp中在函数定义中声明的变量(包括函数参数)的存储持续性为自动的,它们在程序开始执行其所属的函数或者代码块时被创建,在执行完函数或代码时被释放。
  • 静态存储持续性(static):在函数外定义的变量和使用关键字static定义的变量的存储持续性都为静态,它们在整个程序的运行过程中都存在。
  • 线程存储持续性(thread_local):如果变量时使用关键字thread_local声明的,那么其生命周期与所属的线程一样长。
  • 动态存储持续性(new delete):使用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态的。

3.2 变量的存储区域

这里再介绍一下Cpp中的变量存储区域,

  • 栈区(Stack Segment):

局部变量:在函数内部声明的变量,通常存储在栈区。
函数参数:传递给函数的参数,通常也存储在栈区。
特点:栈区内存由编译器自动分配和释放,生命周期是函数调用期间,作用域是当前函数

例如:

1
2
3
void function() {
int localVar = 10; // localVar 存储在栈区
}
  • 堆区(Heap Segment):

动态分配的变量:使用newmalloc动态分配的内存。
特点:堆区内存由程序员手动管理(分配和释放),生命周期是从分配到显式释放(如deletefree)。

例如:

1
2
3
4
void function() {
int* heapVar = new int(10); // heapVar 指向的内存存储在堆区
delete heapVar; // 手动释放内存
}
  • 全局/静态区(Data Segment):

全局变量:在所有函数外部声明的变量。
静态变量:使用 static 关键字声明的变量,无论是在函数内部还是在类内部。
特点:全局/静态区内存在程序开始时分配,程序结束时释放,生命周期贯穿整个程序运行周期。

例如

1
2
3
4
5
int globalVar = 10;  // 全局变量,存储在全局/静态区

void function() {
static int staticVar = 20; // 静态变量,存储在全局/静态区
}
  • 常量区(Text Segment):

字符串字面量和常量:例如字符串字面量、使用 const 关键字定义的常量。
特点:这些常量通常存储在只读内存区,生命周期与程序相同。

例如

1
2
const int constVar = 30;  // 常量,存储在常量区
const char* str = "Hello, World!"; // 字符串字面量,存储在常量区

3.3 作用域

这里介绍一些常见作用域,==代码块==用{}来进行表示,其中变量的默认作用域是局部;==全局作用域的变量==在定义位置开始到文件结尾之间都能使用;==自动变量==的作用域是局部,==静态变量==的作用域是全局还是局部取决于它的定义方式;==类==中声明的成员变量的作用域为整个类。这里举例说明代码块的作用域:

1
2
3
4
5
6
7
8
int main(){
int teledeli = 5;
{
int weight = 2;
std::cout << weight << std::endl;
} // weight变量失效
std::cout << teledeli << std::endl;
} // teledeli变量失效

寄存器变量

关键字register是由C语言进行引入的,它建议编译器使用CPU寄存器来存储自动变量,旨在提高变量的访问速度,例如:

1
register int count_faster;		//请求使用寄存器来存储变量count_faster

3.4 链接性

Cpp中具有三种链接特性

  • 外部链接(其他文件可访问)
  • 内部链接(只能当前文件访问)
  • 无链接(只能当前函数或者代码块访问)

当前文件中的全局变量都是外部链接属性,当前文件的函数均能访问该变量,其他文件可以使用,使用关键字static 可以将其链接属性改为内部链接,例如:

external_linkage.cpp

1
2
3
4
5
6
// 文件1: external_linkage.cpp
int global_external = 10; // 具有外部链接性的全局变量
void external_function() {} // 具有外部链接性的函数

static int global_internal = 20; // 具有内部链接性的全局变量
static void internal_function() {} // 具有内部链接性的函数

函数体或者代码块中的变量默认为无链接属性,只能由当前函数体或者代码块进行访问;外部链接需要关键字extern进行声明,则可以在其他文件中进行使用。例如:

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 文件2: main.cpp
#include <iostream>
#include "external_linkage.cpp"

int main() {
std::cout << global_external << std::endl; // 可以访问具有外部链接性的global_external
external_function(); // 可以调用具有外部链接性的external_function

// std::cout << global_internal << std::endl; // 错误,global_internal具有内部链接性
// internal_function(); // 错误,internal_function具有内部链接性

int local_variable = 30; // 具有无链接性的局部变量
// ...
return 0;
}

3.4.1 extern

C/Cpp中可以使用extern来引用声明变量,与include相比,extern引用另一个文件的范围更小,include可以引用另一个文件的全部内容,extern的引用方式要比包含头文件更加简介。需要注意的是

  • 定义一个extern变量时,关键字extern是可选的
  • 声明一个extern变量时,关键字extern是必须的

举个例子:

file01.cpp

1
2
extern int cats = 20;		// definition because of initialization
int dogs = 22; // definition

file02.cpp

1
2
// use cats from file01.cpp
extern int cats; // declaration

file03.cpp

1
2
3
// use cats, dogs from file01.cpp
extern int cats;
extern int dogs;

3.4.2 static

关键字static可以改变变量或者函数的作用域,需要特别注意一下

  1. 静态局部变量

static 关键字用于函数内部的局部变量时,该变量在函数调用之间保持其值,并且只在第一次调用时初始化一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
void example() {
static int count = 0;
count++;
std::cout << "Count: " << count << std::endl;
}

int main() {
example();
example();
example();
return 0;
}

输出

1
2
3
Count: 1
Count: 2
Count: 3
  1. 静态全局变量

static 关键字用于全局变量时,该变量的作用域仅限于定义它的文件。它不会被其他文件所访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// file1.cpp
#include <iostream>
static int globalVar = 0;

void incrementGlobalVar() {
globalVar++;
std::cout << "GlobalVar: " << globalVar << std::endl;
}

// file2.cpp
extern void incrementGlobalVar();

int main() {
incrementGlobalVar(); // 输出 GlobalVar: 1
incrementGlobalVar(); // 输出 GlobalVar: 2
return 0;
}

注意:file2.cpp 无法直接访问 globalVar

  1. 静态成员变量

在类中,static 关键字用于声明静态成员变量。静态成员变量属于类而不是类的实例。所有类的实例共享同一个静态成员变量。

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

class Example {
public:
static int staticVar;
};

int Example::staticVar = 0;

int main() {
Example::staticVar = 5;
Example obj1;
Example obj2;

std::cout << obj1.staticVar << std::endl; // 输出 5
std::cout << obj2.staticVar << std::endl; // 输出 5

obj1.staticVar = 10;
std::cout << obj2.staticVar << std::endl; // 输出 10

return 0;
}
  1. 静态成员函数

静态成员函数可以在不创建对象的情况下调用,因为它们不属于任何特定对象。静态成员函数只能访问静态成员变量。

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

class Example {
public:
static int staticVar;

static void staticFunction() {
std::cout << "StaticVar: " << staticVar << std::endl;
}
};

int Example::staticVar = 0;

int main() {
Example::staticVar = 5;
Example::staticFunction(); // 输出 StaticVar: 5

Example obj;
obj.staticFunction(); // 输出 StaticVar: 5

return 0;
}
  1. 静态全局函数

在 C++ 中,如果在文件作用域内使用 static 修饰函数(必须同时在函数原型和定义处使用关键字static),该函数的作用域将被限制在定义它的文件中。这种方法用于实现文件私有的函数,避免函数名在多个文件之间冲突。

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

static void staticFunction() {
std::cout << "This is a static function in file1" << std::endl;
}

void callStaticFunction() {
staticFunction();
}

// file2.cpp
#include <iostream>

extern void callStaticFunction();

int main() {
callStaticFunction(); // 输出 This is a static function in file1
return 0;
}

在这个示例中,staticFunction 仅在 file1.cpp 内部可见,无法在 file2.cpp 中直接调用。这避免了同名函数在多个文件中冲突的问题。

总结

  • 静态局部变量:在函数内声明,具有静态存储期,在函数调用之间保持值。
  • 静态全局变量:在文件作用域内声明,仅在定义它的文件中可见。
  • 静态成员变量:在类内声明,属于类而不是类的实例,所有实例共享。
  • 静态成员函数:在类内声明,属于类而不是类的实例,只能访问静态成员。

3.4.3 语言链接性

另一种形式的链接性被称为语言链接性(language linking),这种链接性对程序也有影响。首先解释为什么需要语言链接性,链接程序要求每个不同的函数都有不同的符号名,在C语言中,一个名称只对应一个函数,因此很容易满足,为了满足内部需要,C语言编译器可能将spiff这样的函数名翻译为_spiff,但是在Cpp中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的函数名,因此我们需要指定当前的函数时按照什么方式进行编译的。

Cpp主要提供了两种语言链接性:

  • C语言链接性(C language linkage),使用extern "C" {...}
  • C++语言链接性(C++ language linkage),使用extern "C++" {...}

举个例子

1
2
3
extern "C" {
void c_function(int a, double b);
}

但这不是一种常用的方式,通常我们一个文件中的所有函数都需要使用C语言链接性或者是C++语言链接性,我们可以这样使用

1
2
3
4
5
6
7
8
#ifdef __cpluspluss
extern "C" {
#enif
... //这里存放函数体

#ifdef __cpluspluss
}
#enif

4. 名称空间

Cpp中关于全局变量和局部变量的规则定义了一种名称空间层次,每个声明区域都可声明名称,这些名称独立于在其他声明区域中声明的名称,在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

在C++中,名称空间(Namespace)是一个用来组织和管理代码中标识符(变量、函数、类等)的机制。它可以帮助我们避免命名冲突,提高代码的可读性和可维护性。

4.1 名称空间的定义

  • 名称空间使用namespace关键字定义。
  • 名称空间可以包含变量、函数、类、枚举、typedef等声明。
  • 名称空间可以嵌套,即一个名称空间可以包含另一个名称空间

例如:

1
2
3
4
5
6
7
8
9
namespace MyNamespace {
int variable = 10;
void function() {
// 函数体
}
class MyClass {
// 类定义
};
}

4.2 名称空间的使用

我们需要使用作用域解析运算符::来访问名称空间中的标识符,例如:

1
2
3
MyNamespace::variable = 20;
MyNamespace::function();
MyNamespace::MyClass obj;

也可以直接使用using namespace声明可以将名称空间中的标识符引入到当前作用域,例如

1
2
3
4
using namespace MyNamespace;
variable = 20; // 可以直接使用变量
function(); // 可以直接调用函数
MyClass obj; // 可以直接创建对象

也可以仅引入名称空间中的特定标识符,使用using声明,例如:

1
2
3
4
using MyNamespace::variable;
using MyNamespace::function;
variable = 20; // 可以使用引入的变量
function(); // 可以使用引入的函数

名称空间还支持嵌套,例如:

1
2
3
4
5
6
7
8
9
namespace Outer {
int variable = 10;
namespace Inner {
int variable = 20;
void function() {
std::cout << Outer::variable << " " << variable << std::endl;
}
}
}

Reference

《C++ Prime Plus》