文章目录
- 一、C++ 存储类
- 1. 类的定义
- 2. 对象的创建
- 3. 对象在内存中的布局
- 4. 对象的存储位置
- 二、auto 存储类
- 1. `auto`的基本用法
- 2. `auto`与存储类的关系
- 1) 自动存储类(最常见的)
- 2) 静态存储类
- 3) 动态存储类(通过new)
- 三、register 存储类
- 1. register 存储类的特点
- 2. 案例
- 3. 注意事项
- 四、static 存储类
- 1. 局部静态变量
- 2. 全局静态变量
- 3. 静态类成员变量
- 4. 静态类成员函数
- 五、extern 存储类
- 1. 声明全局变量
- 2. 声明函数
- 3. extern "C"
- 4. 注意事项
- 六、mutable 存储类
- 1. 基本用法
- 2. 示例
- 3. 注意事项
- 七、thread_local 存储类
- 1. 基本用法
- 2. 注意事项
- 八、相关链接
一、C++ 存储类
在C++中,类的存储方式主要涉及到类的定义、对象的创建以及这些对象在内存中的布局。C++是一种静态类型、编译型语言,支持面向对象的编程范式。下面详细解释这些概念:
1. 类的定义
类是C++中用户定义的类型(UDT),用于封装数据(称为成员变量或属性)和函数(称为成员函数或方法)。类的定义不会为对象分配任何内存,它只是描述了对象的结构。
class MyClass {
public:
int x;
void func() {
// 成员函数实现
}
};
2. 对象的创建
当你使用类来创建对象时,才会在内存中为对象分配空间。这个空间用于存储对象的成员变量。成员函数则不直接存储在对象中,而是存储在程序的代码段中,对象通过其成员函数指针(通常是通过隐式的this
指针)来访问这些函数。
MyClass obj; // 创建一个MyClass类型的对象obj
3. 对象在内存中的布局
- 成员变量:对象的成员变量(包括从基类继承的成员变量)按照它们在类中声明的顺序,在内存中连续存储。成员变量的对齐和填充(padding)可能会根据编译器和平台的不同而有所不同,以优化内存访问速度。
- 成员函数:成员函数不直接存储在对象中,而是存储在程序的代码段(Code Segment)中。当对象调用成员函数时,该函数通过隐式的
this
指针来访问对象的成员变量。 - 静态成员:静态成员变量在程序的全局数据段中分配一次内存,所有对象共享同一份静态成员变量的拷贝。静态成员函数则不直接访问任何对象的成员变量(除非通过对象或类名显式传递),因此它们没有
this
指针。
4. 对象的存储位置
对象的存储位置取决于其生命周期和作用域:
- 自动存储期:在函数或代码块内声明的局部变量(包括对象)存储在栈(Stack)上,其生命周期仅限于定义它们的代码块。
- 静态存储期:全局对象、静态局部变量以及静态成员变量存储在全局数据段(Global Data Segment)或静态数据段(Static Data Segment)中,它们在程序的整个执行期间都存在。
- 动态存储期:通过
new
操作符动态分配的对象存储在堆(Heap)上,其生命周期由new
和delete
操作符控制。
二、auto 存储类
在C++中,auto
关键字不是用来直接声明存储类的,而是用来自动推导变量类型的。存储类(也称为存储期或生命周期)指的是变量在内存中保留的时间长度,常见的存储类有自动(automatic)、静态(static)、寄存器(register,但现代C++中很少使用,且其优化行为通常由编译器自动处理)、外部(extern)以及动态分配(通过new
操作符)。
然而,当我们在C++中使用auto
关键字时,我们是在告诉编译器自动根据初始化表达式的类型来推导变量的类型。这并不意味着auto
变量就有特定的存储类;它的存储类取决于其声明的上下文。
1. auto
的基本用法
下面是一个使用auto
的简单案例,它演示了如何自动推导变量的类型,但并没有直接涉及到存储类的概念:
#include <iostream>
#include <vector>
int main() {
// 自动推导为int类型
auto a = 10;
std::cout << "a is: " << a << std::endl;
// 自动推导为std::vector<int>类型
std::vector<int> v = {1, 2, 3, 4, 5};
auto b = v; // 注意:这里b是v的一个拷贝,而不是引用
std::cout << "b has " << b.size() << " elements." << std::endl;
return 0;
}
2. auto
与存储类的关系
尽管auto
本身不直接声明存储类,但auto
声明的变量可以根据其声明的上下文获得不同的存储类。以下是一些例子:
1) 自动存储类(最常见的)
void func() {
auto x = 10; // x具有自动存储类,因为它是在函数内部声明的局部变量
// ...
} // x的作用域结束,x的生命周期也结束
2) 静态存储类
static auto y = 20; // y具有静态存储类,尽管它是用auto声明的
// y的生命周期贯穿整个程序执行期间,但其作用域可能受限于声明它的块或文件
void func() {
// 可以在这里访问y
}
注意:在全局或命名空间作用域中使用static
会改变链接性(linkage),但在函数内部使用时,它主要影响生命周期和初始化时机(只初始化一次)。
3) 动态存储类(通过new)
虽然auto
通常不直接与动态分配相关联,但你可以使用auto
来接收new
表达式的结果:
auto ptr = new int(30); // ptr是指向int的指针,具有自动存储类
// 但它指向的对象是通过new动态分配的,具有动态存储类
// 记得用delete ptr;来释放内存
在这个例子中,ptr
本身是一个具有自动存储类的局部变量,但它指向的对象是通过new
动态分配的,因此该对象具有动态存储类。
总结:auto
用于自动推导变量类型,而变量的存储类取决于其声明的上下文(如是否在函数内部、是否使用了static
关键字、是否通过new
动态分配等)。
三、register 存储类
在C++中,register
存储类是一个特殊的说明符,用于向编译器建议将变量存储在CPU的寄存器中,以便快速访问。然而,需要注意的是,register
关键字仅仅是一个建议,并不保证变量一定会被存储在寄存器中。此外,从C++11开始,register
关键字被废弃,现代编译器通常会忽略它,因为它们有自己的优化机制来决定变量的存储位置。
1. register 存储类的特点
- 建议性:
register
关键字告诉编译器,程序员认为该变量应该被频繁访问,因此建议将其存储在寄存器中。但编译器可以忽略这个建议。 - 局部变量:
register
只能用于局部变量,不能用于全局变量或静态变量。 - 限制:使用
register
的变量不能被取地址(即不能应用一元&
运算符),因为它可能没有内存地址。 - 尺寸限制:
register
变量的最大尺寸通常等于寄存器的大小(通常是一个词或几个词)。
2. 案例
尽管 register
关键字在现代C++编程中不再被推荐使用,但以下是一个简单的例子,展示了如果编译器支持 register
关键字时它的用法:
#include <iostream>
int main() {
register int counter = 0; // 建议编译器将counter存储在寄存器中
for (int i = 0; i < 1000000; i++) {
counter++; // 假设counter被存储在寄存器中,这可能会提高循环的性能
}
std::cout << "Counter: " << counter << std::endl;
return 0;
}
然而,在现代C++编译器中,即使你使用了 register
关键字,编译器也可能会忽略它,因为它有自己的优化策略来决定哪些变量应该被存储在寄存器中。
3. 注意事项
- 废弃:由于
register
关键字在C++11中被废弃,因此在新的C++代码中应避免使用它。 - 性能优化:如果你关心性能,并且想要优化特定变量的访问速度,最好让编译器自己处理这些优化,或者通过其他方式(如使用更高效的算法或数据结构)来提高程序性能。
- 代码可读性:使用
register
关键字可能会降低代码的可读性,因为它引入了一个不再被现代编译器广泛支持的特性。
综上所述,虽然 register
存储类在理论上可以用于优化变量访问速度,但在现代C++编程中,它已经不再是一个有用的特性,应该被避免使用。
四、static 存储类
在C++中,static
存储类是一个非常重要的特性,它用来控制变量的存储方式和可见性。以下是static
存储类的详细解释:
1. 局部静态变量
当static
用于函数内的局部变量时,它会使该变量具有静态存储期,即变量在程序的整个执行期间都存在,但只在定义它的作用域内可见。这样的变量只初始化一次,每次函数调用时保留其最后一次使用的值。
特点:
- 存储在静态存储区。
- 只在定义它的函数内可见。
- 即使函数调用结束,也不会销毁。
- 只初始化一次。
示例:
#include <iostream>
using namespace std;
void countFunction() {
static int count = 0; // 局部静态变量
count++;
cout << "Count: " << count << endl;
}
int main() {
countFunction(); // 输出 1
countFunction(); // 输出 2
countFunction(); // 输出 3
return 0;
}
2. 全局静态变量
在函数外部使用static
声明的全局变量或函数,会限制其链接性(可见性),使其只在定义它的文件中可见。这有助于避免不同源文件之间的命名冲突。
特点:
- 存储在全局数据段。
- 只在其定义的文件内可见。
- 在程序的整个执行期间都存在。
示例:
// file1.cpp
#include <iostream>
static int globalValue = 10;
void display() {
std::cout << "Global Value in file1: " << globalValue << std::endl;
}
// file2.cpp
// extern int globalValue; // 如果取消注释,则会导致编译错误,因为globalValue在file2中不可见
// 假设有main函数在file2.cpp或其他地方
// 调用display函数会正常显示file1中的globalValue值
3. 静态类成员变量
在类中声明static
变量时,该变量不属于任何特定的对象实例,而是类的所有实例共享的。它们必须在类外部初始化。
特点:
- 存储在静态存储区。
- 类的所有对象共享同一份拷贝。
- 初始化时使用作用域运算符(::)来标明它所属类。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
static int staticValue;
};
int MyClass::staticValue = 0; // 初始化
int main() {
MyClass obj1, obj2;
obj1.staticValue = 5;
cout << "Static Value from obj1: " << obj1.staticValue << endl; // 输出 5
cout << "Static Value from obj2: " << obj2.staticValue << endl; // 输出 5
return 0;
}
4. 静态类成员函数
静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。它们可以在没有对象实例的情况下调用,并且只能访问其类的静态成员。
特点:
- 没有
this
指针。 - 只能访问类的静态成员变量和静态成员函数。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
static int staticValue;
static void displayValue() {
cout << "Static Value: " << staticValue << endl;
}
};
int MyClass::staticValue = 10;
int main() {
MyClass::displayValue(); // 输出 10
return 0;
}
五、extern 存储类
在C++中,extern
关键字是一个非常重要的存储类修饰符,它主要用于声明一个全局变量或函数,这些变量或函数是在其他文件中定义的。下面详细解释extern
存储类的用法和特点:
1. 声明全局变量
当我们在多个文件中使用同一个全局变量时,extern
可以帮助我们实现这一目标。通过extern
,我们可以在一个文件中声明该全局变量(而不是定义它,即不分配内存),然后在其他文件中使用它。这样做的好处是避免了在多个文件中重复定义同一个全局变量可能导致的编译错误。
示例:
假设我们有两个文件,main.cpp
和support.cpp
。
- 在
support.cpp
中定义一个全局变量:
int testVar = 0; // 定义一个全局变量
- 在
main.cpp
中,我们通过extern
来声明这个全局变量,以便使用它:
extern int testVar; // 声明外部变量
int main() {
testVar = 10; // 使用外部变量
// ...
}
2. 声明函数
除了变量之外,extern
也可以用于声明在其他文件中定义的函数。这样,我们就可以在多个文件中调用同一个函数,而不需要在每个文件中都重复函数的定义。
示例:
- 在
support.cpp
中定义一个函数:
void testFunc() {
// 函数体
}
- 在
main.cpp
中,我们通过extern
来声明这个函数,以便调用它:
extern void testFunc(); // 声明外部函数
int main() {
testFunc(); // 调用外部函数
// ...
}
然而,需要注意的是,在C++中,对于函数的声明,extern
通常是隐式的,即我们不需要显式地写出extern
来声明一个函数是在其他文件中定义的。但在某些特殊情况下,为了强调或明确函数的外部链接性,也可以显式地使用extern
。
3. extern “C”
在C++中,我们还会经常看到extern "C"
的用法。这是因为C++支持函数重载,而C语言不支持。当C++代码需要被C语言代码调用时,为了避免链接错误(即C++编译器对函数名进行名称修饰,而C编译器不进行),我们需要用extern "C"
来告诉C++编译器按照C语言的方式来链接这些函数。
示例:
extern "C" void testFunc() {
// 函数体
}
或者,在头文件中声明时:
#ifdef __cplusplus
extern "C" {
#endif
void testFunc();
#ifdef __cplusplus
}
#endif
4. 注意事项
extern
只是声明,不是定义。它告诉编译器变量或函数的类型,但不会分配内存。extern
声明的变量或函数必须在某个文件中被定义,否则在链接阶段会报错。extern
只能用于全局变量和函数,不能用于函数内部的局部变量。- 在C++中,对于函数的声明,
extern
通常是隐式的,但对于全局变量或需要明确指出外部链接性的函数,extern
是必需的。
六、mutable 存储类
在C++中,mutable
关键字是一个特殊的存储类修饰符,它用于类的成员变量上,以允许这个成员变量即使在常量成员函数(即被const
修饰的成员函数)中也能被修改。这个特性在需要缓存计算结果、跟踪对象状态变化等场景中非常有用,特别是当这些变化不应该影响对象的逻辑常量性时。
1. 基本用法
当你将mutable
关键字应用于类的成员变量时,这个成员变量即使在常量成员函数中也可以被修改。这意呀着,常量成员函数(即被声明为const
的成员函数)可以修改这个mutable
成员变量的值,而不会违反const
成员函数不应修改任何成员变量的常规规则。
2. 示例
#include <iostream>
class MyClass {
private:
mutable int cache; // 使用mutable修饰,允许在const成员函数中被修改
int value;
public:
MyClass(int v) : value(v), cache(-1) {} // 初始化value,cache初始化为-1表示未计算
// 常量成员函数,但可以修改mutable成员变量cache
int getValue() const {
if (cache == -1) { // 如果cache未被计算
cache = computeValue(); // 计算并存储结果
}
return cache;}
// 非const成员函数,可以修改所有成员变量
void setValue(int v) {
value = v;
cache = -1; // 重置cache,因为value的改变可能影响计算结果
}
private:
// 假设这是一个耗时的计算过程
int computeValue() const {
// ... 进行计算 ...
return value * 2; // 仅为示例
}
};
int main() {
MyClass obj(5);
std::cout << "Value: " << obj.getValue() << std::endl; // 输出:Value: 10
obj.setValue(10);
std::cout << "New Value: " << obj.getValue() << std::endl; // 输出:New Value: 20
return 0;
}
3. 注意事项
mutable
只能用于类的成员变量上。- 使用
mutable
要谨慎,因为它可能会让代码的阅读者感到困惑,特别是当常量成员函数实际上修改了某些状态时。因此,建议只在确实需要时才使用mutable
,并且确保在文档中清楚地说明这一点。 mutable
成员变量通常用于缓存计算结果、状态跟踪等场景,这些场景中的变化不会影响对象的逻辑常量性。mutable
成员变量不会改变常量成员函数的const
性质,即常量成员函数仍然不能修改类的非mutable
成员变量。
七、thread_local 存储类
在C++11及以后的版本中,thread_local
关键字被引入作为一个存储类修饰符,用于声明线程局部存储(Thread-Local Storage, TLS)的变量。这意味着每个线程都有该变量的一个独立实例,线程之间对这些变量的修改是互不影响的。
1. 基本用法
当你在变量声明前加上thread_local
关键字时,这个变量就变成了线程局部的。每个线程对这个变量的访问都指向它自己的独立实例。
#include <iostream>
#include <thread>
thread_local int tls_var = 0; // 线程局部变量
void increment() {
tls_var++; // 每个线程都会增加它自己的tls_var实例
std::cout << "Thread " << std::this_thread::get_id() << " sees tls_var as " << tls_var << std::endl;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
increment(); // 主线程也会执行
t1.join();
t2.join();
return 0;
}
在这个例子中,每个线程(包括主线程)都会打印出它自己的tls_var
值,这些值在每次调用increment
函数时都会递增,但每个线程看到的都是它自己的独立副本。
2. 注意事项
thread_local
变量在其所属线程开始时被构造,在线程结束时被销毁。这意味着你不能在全局作用域中直接初始化一个需要构造函数和析构函数的thread_local
变量,因为全局变量的构造和析构发生在程序开始和结束时,而这时线程可能还没有开始或已经结束。thread_local
变量不会跨线程共享,这意呀着每个线程对其的修改都不会影响到其他线程。- 在某些平台上,
thread_local
变量的实现可能依赖于动态内存分配,这可能会引入额外的性能开销。此外,如果线程数量非常多,可能会消耗大量的内存。 - 在使用
thread_local
时,要特别注意内存泄漏和异常安全问题。如果thread_local
变量在析构时抛出异常,并且这个异常没有被捕获,那么程序可能会以未定义的方式终止。 - 并不是所有的C++实现都支持
thread_local
,但它被C++11标准所要求,因此大多数现代编译器都支持它。
八、相关链接
- Visual Studio Code下载地址
- Sublime Text下载地址
- 「C++系列」C++简介、应用领域
- 「C++系列」C++ 基本语法
- 「C++系列」C++ 数据类型
- 「C++系列」C++ 变量类型
- 「C++系列」C++ 变量作用域
- 「C++系列」C++ 常量知识点-细致讲解
- 「C++系列」C++ 修饰符类型