基本概念-指针¶
stack and heap¶

Data/BSS (Block Started by Symbol) segments are larger than stack memory (max ≈ 1GB in general) but slower
| Stack | Heap | |
|---|---|---|
| Memory Organization |
Contiguous (LIFO) | Contiguous within an allocation, Fragmented between allocations (relies on virtual memory) |
| Max size | Small (8MB on Linux, 1MB on Windows) |
Whole system memory |
| If exceed | Program crash at function entry (hard to debug) |
Exception or nullptr |
| Allocation | Compile-time | Run-time |
| Locality | High | Low |
| Thread View | Each thread has its own stack | Shared among threads |
stack¶
int x = 3; // not on the stack (data segment)
struct A {
int k; // depends on where the instance of A is
};
int main() {
int y = 3; // on stack
char z[] = "abc"; // on stack
A a; // on stack (also k)
void* ptr = malloc(4); // variable "ptr" is on the stack
}
- 在
stack上的 data:- 局部变量 (local variable)
- 函数参数 (Function arguments)
- 编译器临时变量 (Compiler temporaries)
- 中断上下文 (Interrupt contexts)
注意: 存储在栈 (stack) 中的每个对象在其作用域之外都是无效的!
int* f() {
int array[3] = {1, 2, 3};
return array;
}
int* ptr = f();
cout << ptr[0]; // Illegal memory access!!
void g(bool x) {
const char* str = "abc";
if (x) {
char xyz[] = "xyz";
str = xyz;
}
cout << str; // if "x" is true, then Illegal memory access!!
}
heap¶
学习堆 (heap) 的用法,关键是掌握 new 和 delete。注意,用到 new/new[] ,就必须有对应的 delete/delete[];用到 malloc,就必须用 free 。
new/new[] 和 delete/delete[] 是 C++ 的关键字,它们在运行时执行动态内存分配/释放以及对象的构造/析构。
malloc 和 free 是 C 语言中的函数,它们只负责分配和释放内存块(以字节为单位), 不涉及调用对象的构造函数和析构函数。同时,new 返回确切的数据类型,而 malloc() 返回 void*
new 和 delete 的优点:
1. 是语言的关键字,而不是函数。这意味着它们是语言的一部分,可以提供比 C 语言中的库函数 malloc 和 free 更安全的错误处理和类型安全。
2. new 直接返回对象的确切类型的指针,无需类型转换。 相比之下,malloc 返回一个 void* 类型的指针,使用时通常需要转换到适当的类型,这增加了出错的风险。
3. 当内存分配失败时,new 会抛出一个异常(std::bad_alloc),这迫使开发者处理这种情况,避免了忽视错误的可能。 malloc 在分配失败时返回 NULL,需要开发者显式检查返回值以确认是否成功,这容易被忽略。
4. 使用 new 时,编译器自动计算所需的内存大小,用户不需要关心。使用 malloc 时,必须手动计算并指定需要分配的字节数,这不仅繁琐,也容易出错。
5. new 不仅用于内存分配,还可以在分配内存时初始化对象。例如,new int(5) 会分配一个整数并初始化为 5。malloc 只负责内存分配,不进行任何初始化。
6. 在 C++ 中,使用 new 分配具有虚函数的对象时,会自动设置虚拟表指针(vptr),确保对象的多态性正常工作。malloc 由于只是简单地分配内存,无法处理虚拟表的初始化,因此不能用于需要支持多态性的对象。
分配单一元素
int* value = (int*) malloc(sizeof(int)); // C
int* value = new int; // C++
分配 N 个元素
int* array = (int*) malloc(N * sizeof(int)); // C
int* array = new int[N]; // C++
分配 N 个 struct
MyStruct* array = (MyStruct*) malloc(N * sizeof(MyStruct)); // C
MyStruct* array = new MyStruct[N]; // C++
分配并 zero-initialize N 个元素
int* array = (int*) calloc(N, sizeof(int)); // C
int* array = new int[N](); // C++
释放单个元素
int* value = (int*) malloc(sizeof(int)); // C
free(value);
int* value = new int; // C++
delete value;
释放 N 个元素
int* value = (int*) malloc(N * sizeof(int)); // C
free(value);
int* value = new int[N]; // C++
delete[] value;
int** A = new int*[3]; // array of pointers allocation
for (int i = 0; i < 3; i++)
A[i] = new int[4]; // inner array allocations
for (int i = 0; i < 3; i++)
delete[] A[i]; // inner array deallocations
delete[] A; // array of pointers deallocation
auto A = new int[3][4]; // allocate 3 objects of type int[4]
int n = 3; // dynamic value
auto B = new int[n][4]; // ok
// auto C = new int[n][n]; // compile error
delete[] A; // same for B, C
内存泄漏: 内存泄漏是指堆内存中已经分配出来但程序不再使用,且在整个执行过程中仍然保持分配状态的实体。
问题: - 非法内存访问 → 导致段错误/错误结果 (segmentation fault/wrong results) - 未定义值及其传播 → 导致段错误/错误结果 (segmentation fault/wrong results) - 额外的内存消耗(可能导致段错误)
int main() {
int* array = new int[10];
array = nullptr; // memory leak!!
} // the memory can no longer be deallocated!!
int* x = new int; // 分配内存以存储一个整数
int num_iters = 4096 / sizeof(int); // 计算每个整数所占字节数,用4KB内存可以存放的整数数量
for (int i = 0; i < num_iters; i++)
x[i] = 1; // 在未分配足够内存的情况下访问和修改内存
x 分配内存时,操作系统实际可能分配了至少一个完整的页,即至少 4KB 的内存,这使得对 x 后面的内存的写操作不会立即导致访问违规,因为从操作系统的角度看,这块内存是属于进程的。
指针¶
指针 T* 是一个引用内存位置的值。
指针解引用 Pointer dereferencing (*ptr) 指的是获取存储在指针引用的位置的值。
下标操作符 subscript operator (ptr[]) 允许访问给定位置的指针元素。
address-of 运算符 & 返回变量的地址
指针的类型(例如 void*)取决于底层架构,是一个32位或64位的无符号整数。仅支持操作符 +, -, ++, --, 比较 ==, !=, <, <=, >, >=, 下标操作符 [] 以及解引用操作符 *。
指针可以显式转换为整数类型。
void* x;
size_t y = (size_t) x; // 可以(显式转换)
// size_t y = x; // 编译错误(隐式转换)
指针转换¶
- 任何指针类型都可以隐式地转换为
void*。 - 非
void指针必须显式转换。 - 出于安全原因,
static_cast不允许进行指针类型转换,除非是转换为void*。int* ptr1 = ...; void* ptr2 = ptr1; // int* -> void*,隐式转换 void* ptr3 = ...; int* ptr4 = (int*) ptr3; // void* -> int*,需要显式转换 // static_cast 是允许的 int* ptr5 = ...; char* ptr6 = (char*) ptr5; // int* -> char*,需要显式转换, // static_cast 不允许,危险
解引用 dereferencing¶
int* ptr1 = new int;
*ptr1 = 4; // 解引用(赋值)
int a = *ptr1; // 解引用(取值)
数组的下标操作
int* ptr2 = new int[10];
ptr2[2] = 3;
int var = ptr2[4];
指针运算¶
-
下标操作的含义
ptr[i]等价于*(ptr + i)下标操作符也接受负数值。 -
指针算数规则
address(ptr + i) = address(ptr) + (sizeof(T) * i)
int array[4] = {1, 2, 3, 4};
cout << array[1]; // print 2
cout << *(array + 1); // print 2
cout << array; // print 0xFFFAFFF2
cout << array + 1; // print 0xFFFAFFF6!!
int* ptr = array + 2;
cout << ptr[-1]; // print 2

引用¶
变量引用 reference T& 是一个别名,即已存在变量的另一个名称。变量和变量引用都可以用来引用变量的值。
- 指针具有自己的内存地址和大小,并存储在栈上,而引用则共享原始变量的内存地址。
- 编译器可以在内部将引用实现为指针,但以非常不同的方式处理它们。
引用比指针更安全: - 引用不能拥有 NULL 值。你必须始终假设一个引用已经与一个合法的存储区域相连。 - 引用不能被更改。一旦一个引用初始化为指向某个对象,它就不能被更改为指向另一个对象。(指针可以在任何时候指向另一个对象) - 引用在创建时必须初始化。(指针可以在任何时候初始化)
-
左值引用:
T&只能引用左值,可以修改左值 -
左值/右值引用:
const T&可以引用左值或右值,可以延长右值的生存周期, 但不能修改左值/右值 -
右值引用:
T&&只绑定右值, 可以引用并修改右值
//int& a; // compile error no initialization
//int& b = 3; // compile error "3" is not a variable
int c = 2;
int& d = c; // reference. ok valid initialization
int& e = d; // ok. the reference of a reference is a reference
++d; // increment
++e; // increment
cout << c; // print 4
int a = 3;
int* b = &a; // pointer
int* c = &a; // pointer
++b; // change the value of the pointer 'b'
++*c; // change the value of 'a' (a = 4)
int& d = a; // reference
++d; // change the value of 'a' (a = 5)
-
reference vs pointer arguments:
void f(int* value) {} // value may be a nullptr void g(int& value) {} // value is never a nullptr int a = 3; f(&a); // ok f(0); // dangerous but it works!! (but not with other numbers) //f(a); // compile error "a" is not a pointer g(a); // ok //g(3); // compile error "3" is not a reference of something //g(&a); // compile error "&a" is not a reference -
引用可以指定数组长度
void f(int (&array)[3]) { // accepts only arrays of size 3
cout << sizeof(array);
}
void g(int array[]) {
cout << sizeof(array); // any surprise?
}
int A[3], B[4];
int* C = A;
//------------------------------------------------------
f(A); // ok
// f(B); // compile error B has size 4
// f(C); // compile error C is a pointer. 当数组作为函数参数传递时,它通常会退化为一个指向其第一个元素的指针。这就是为什么在很多情况下数组表现得像指针。
g(A); // ok
g(B); // ok
g(C); // ok
constants¶
const 关键字声明了一个在初始化之后其值不再改变的对象。const 变量在声明时必须初始化。如果右侧表达式在编译时也被求值,则 const 变量是在编译时求值的。
int size = 3; // 'size' 是动态的
int A[size] = {1, 2, 3}; // 从技术上讲是可能的,但是,变量大小的栈数组
// 被认为是糟糕的编程实践
const int SIZE = 3;
// SIZE = 4; // 编译错误,SIZE 是 const
int B[SIZE] = {1, 2, 3}; // 正确
const int size2 = size; // 'size2' 是动态的
int* 指向 int 的指针
- 指针的值可以被修改
- 指针指向的元素可以被修改
const int* 指向 const int 的指针。读作 (const int)*
- 指针的值可以被修改
- 指针指向的元素不能被修改
int *const 指向 int 的 const 指针
- 指针的值不能被修改
- 指针指向的元素可以被修改
const int *const 指向 const int 的 const 指针
- 指针的值不能被修改
- 指针指向的元素不能被修改
注释:const int*(西方记法)等同于 int const*(东方记法)
注意: 给指针添加 const 与给指针类型别名添加 const 不是一回事
using ptr_t = int*;
using const_ptr_t = const int*;
void f1(const int* ptr) { // read as '(const int)*'
// ptr[0] = 0; // not allowed: pointer to const objects
ptr = nullptr; // allowed
}
void f2(const_ptr_t ptr) {} // same as before
void f3(const ptr_t ptr) { // warning!! equal to 'int* const'
ptr[0] = 0; // allowed!!
// ptr = nullptr; // not allowed: const pointer to modifiable objects
}
constexpr¶
constexpr 说明符声明了一个可以在编译时求值的表达式
- const 保证变量的值在初始化期间是固定的
- constexpr 隐含了 const
- constexpr 可以提高性能和内存使用效率
- constexpr 可能影响编译时间
- constexpr 变量总是在编译时求值
const int v1 = 3; // compile-time evaluation
const int v2 = v1 * 2; // compile-time evaluation
int a = 3; // "a" is dynamic
const int v3 = a; // run-time evaluation!!
constexpr int c1 = v1; // ok
// constexpr int c2 = v3; // compile error, "v3" is dynamic
constexpr 就会保证在编译时求值
constexpr int square(int value) {
return value * value;
}
square(4); // 编译时求值,'4' 是一个字面量
int a = 4; // "a" 是动态的
square(a); // 运行时求值
constexpr 的非静态成员函数,如果包含数据成员或非编译时函数,那么在运行时对象中不能在编译时使用,因为实例要在运行时创建
静态 constexpr 成员函数则不会出现这个问题,因为它们不依赖于具体的实例,在编译时就可以完全确定
struct A {
int v { 3 };
constexpr int f() const { return v; }
static constexpr int g() { return 3; }
};
A a1;
// constexpr int x = a1.f(); // 编译错误,f() 在运行时求值
constexpr int y = a1.g(); // 正确,等同于 'A::g()'
constexpr A a2;
constexpr int x = a2.f(); // 正确