Skip to content

基本概念-指针

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) 的用法,关键是掌握 newdelete。注意,用到 new/new[] ,就必须有对应的 delete/delete[];用到 malloc,就必须用 free

new/new[]delete/delete[] 是 C++ 的关键字,它们在运行时执行动态内存分配/释放以及对象的构造/析构。

mallocfree 是 C 语言中的函数,它们只负责分配和释放内存块(以字节为单位), 不涉及调用对象的构造函数和析构函数。同时,new 返回确切的数据类型,而 malloc() 返回 void*

newdelete 的优点: 1. 是语言的关键字,而不是函数。这意味着它们是语言的一部分,可以提供比 C 语言中的库函数 mallocfree 更安全的错误处理和类型安全。 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;
2D 数组的分配
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
2D 数组的分配 (c++11)
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!!
程序本身并不直接分配内存,而是向操作系统请求一块内存。操作系统以内存页(虚拟内存)的粒度提供内存,例如Linux上的4KB。
int* x = new int;  // 分配内存以存储一个整数
int num_iters = 4096 / sizeof(int); // 计算每个整数所占字节数,用4KB内存可以存放的整数数量
for (int i = 0; i < num_iters; i++)
    x[i] = 1; // 在未分配足够内存的情况下访问和修改内存
尽管代码试图访问未分配的内存,却没有发生段错误(segmentation fault),这是因为操作系统通常以页(通常大小为4KB)为单位分配内存。当为 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(); // 正确