Skip to content

全局变量、局部变量、静态局部变量、静态全局变量 ​

全局变量在一个文件中声明,但是可以作用于所有文件,当然其他文件需要加extern 来声明 局部变量,具有局部作用域,然后在函数生命周期内有效 静态局部变量,具有局部作用域,但是第一次定义后整个程序期间都有效,但对其他函数不可见,适合函数运行在程序生命周期的变量,比如状态机 静态全局变量,用来保证该变量对其他文件是不可见的,只在定义它的文件时可见的,即使函数重复定

const 作用和用法 ​

修饰变量表示这个变量是不可以修改的 修饰函数参数表示这个函数不可以修改这个传递进来的参数 修改成员函数表示这个函数不可以修改其他成员变量的值

C++程序崩溃会core dump一个栈,这个栈的内容是什么 ​

函数调用链:从崩溃的函数开始,一层层的往上列所有调用的函数 每个函数的地址:记录每个函数在内存的具体地址,结合编译的符号表,可以对应具体那个函数 函数的参数和局部变量:会保存这个函数传入的参数值,以及函数内部定义的临时变量 栈指针的位置:记录崩溃时栈顶的位置,告诉你当前栈用到了哪里,有没有溢出

拷贝构造函数 ​

第一个参数是自身类类型的引用(通常是 const 引用),其余参数都有默认值。因参数需引用,否则传参会无限递归。不写则编译器合成一个,逐成员拷贝。

拷贝初始化:T a = b; 传非引用参数、返回非引用值时都会触发,靠拷贝构造完成。explicit 修饰的函数禁止隐式转换。

cpp
class Foo {
public:
    Foo() = default;
    Foo(const Foo&);  // 拷贝构造函数
};
// 拷贝构造函数的调用方式
Foo a;
Foo b(a);               // 直接初始化,调用拷贝构造
Foo c = a;              // 拷贝初始化,用 = 定义变量
void f(Foo x); f(a);    // 对象作为实参传给非引用形参
Foo g() { Foo t; return t; }  // 从非引用返回类型返回对象
Foo arr[] = {a, b};     // 花括号列表初始化数组/聚合类成员
vector<Foo> v; v.push_back(a); // 容器 insert/push 元素

拷贝消除,就是编译器优化的一种方式:拷贝初始化时,编译器可以省掉中间那次拷贝构造,直接构造对象(但被省掉的拷贝构造仍必须存在且可访问)。

合成拷贝构造函数 ​

未定义拷贝构造时编译器合成一个(即使已定义了其他构造函数也照样合成)。逐个拷贝非 static 成员:类类型调其拷贝构造,内置类型直接拷贝,数组逐元素拷贝。某些类的合成版会被定义为删除以禁止拷贝。就是默认拷贝构造函数。

cpp
// 等价于 Sales_data 的合成拷贝构造
Sales_data::Sales_data(const Sales_data &orig):
    bookNo(orig.bookNo),          // string 的拷贝构造
    units_sold(orig.units_sold),  // 直接拷贝
    revenue(orig.revenue) { }     // 直接拷贝

浅拷贝 合成拷贝对指针成员只复制指针值,两对象指向同一块内存。析构时会 double free,一方修改也会影响另一方。

cpp
class Str {
    char* data;
public:
    Str(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); }
    ~Str() { delete[] data; }   // 无自定义拷贝构造,用合成版
};

{
    Str a("hi");
    Str b = a;      // 浅拷贝: b.data == a.data,指向同一内存
}                   // 作用域结束: b 先析构 delete[] data,
                    // a 再析构又 delete[] 同一指针 —— double free 崩溃

深拷贝

cpp
class Str {
    char* data;
public:
    Str(const Str& o) {                 // 深拷贝
        data = new char[strlen(o.data)+1];
        strcpy(data, o.data);           // 独立副本
    }
    ~Str() { delete[] data; }
};

运算符重载(基本概念) ​

名字为 operator@ 的函数,参数数量=运算对象数量。成员函数左侧对象绑定 this,显式参数少一个。除 operator() 外不能有默认实参。

  • 必须是类成员或至少含一个类类型参数,不能给内置类型重定义;不能造新符号;优先级结合律同内置。
  • 不能重载:::、.*、.、?:;不建议重载:,、&、&&、||(丢失求值顺序和短路)。
  • 必须是成员:=、[]、()、->;一般成员:复合赋值、++/--/*;一般非成员:算术、关系、相等、位运算(对称、支持两端转换)。

"hi" + s 能成立是因为 string 的 + 是非成员:等价 operator+("hi", s),两端可转 string;若为成员则 "hi".operator+(s) 出错(const char* 无成员函数)。

什么是指针 ​

指针是一个变量,它保存的值是另一个对象的内存地址。通过这个地址间接访问那块内存,这是 C/C++ 能直接操作内存的基础。

指针本身也是对象,有自己的地址和大小。大小只取决于机器的地址宽度,与它指向的类型无关:64 位平台上任何指针都是 8 字节。

cpp
int a = 42;
int* p = &a;      // & 取地址,p 保存 a 的地址
cout << *p;       // * 解引用,读出 42
*p = 100;         // 通过指针写,a 变成 100
cout << sizeof(p);        // 8(64 位)
cout << sizeof(double*);  // 8,指向什么都一样

指针的类型决定解引用时"按什么方式解释这块内存"以及指针算术的步长,这是编译期的事,不影响指针自身的大小。

指针的四种状态 ​

任意时刻一个指针处于四种状态之一:

  1. 指向一个对象
  2. 指向对象的下一个位置(尾后指针,如 arr + n,可比较不可解引用)
  3. 空指针,不指向任何对象
  4. 无效指针,以上都不是

解引用后两种是未定义行为。空指针至少还能判断,无效指针连判断都做不到,所以定义指针时就该初始化。

cpp
int* p1 = nullptr;   // C++11 推荐
int* p2 = NULL;      // 老写法,本质是宏 0
int* p3 = 0;         // 合法但不建议
int* p4;             // 危险:值是随机的,野指针

nullptr 优于 NULL,因为 NULL 就是整数 0,碰上重载会选错:

cpp
void f(int);
void f(char*);
f(NULL);      // 调 f(int),不是想要的那个
f(nullptr);   // 调 f(char*)

指针与引用的区别 ​

面试高频。核心:引用是别名,指针是对象。

指针引用
是否是对象是,有自己的地址不是,只是别名
必须初始化否(但应该)是
能否重新绑定能,随时改指向不能,从生到死只绑一个对象
能否为空能,nullptr不能
多级有 int**没有引用的引用
sizeof指针大小(8)被引对象的大小
cpp
int a = 1, b = 2;
int* p = &a;  p = &b;   // OK,改指向
int& r = a;   r = b;    // 不是重新绑定,是把 b 的值赋给 a

引用本质是编译器帮你做的自动解引用,多数实现底层也用指针,但语言层面它没有独立身份。

const 和指针 ​

const 位置不同含义完全不同,看 * 的左右:* 左边修饰所指的对象,右边修饰指针本身。

cpp
int a = 1, b = 2;

const int* p1 = &a;    // 指向常量的指针:不能改 *p1,能改 p1
int* const p2 = &a;    // 常量指针:能改 *p2,不能改 p2
const int* const p3 = &a;  // 都不能改

*p1 = 10;   // 错误
p1 = &b;    // OK
*p2 = 10;   // OK
p2 = &b;    // 错误

从右往左读就顺了:int* const p2 读作"p2 是一个 const 的、指向 int 的指针"。

指针算术与数组 ​

指针加减以所指类型的大小为单位,不是以字节为单位。数组名在多数表达式中会退化成指向首元素的指针,这就是 arr[i] 等价于 *(arr + i) 的原因。

cpp
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;         // 等价于 &arr[0]
cout << *(p + 2);     // 30,地址前进 2*4=8 字节
cout << p[2];         // 30,下标就是语法糖
cout << arr + 5 - p;  // 5,尾后指针可参与运算与比较

只有指向同一数组内部(含尾后位置)的两个指针相减才有定义,结果类型是 ptrdiff_t。跨数组做算术是未定义行为。

数组退化也解释了为什么函数参数里的数组拿不到长度:

cpp
void f(int arr[10]) {      // 等价于 void f(int* arr)
    cout << sizeof(arr);   // 8,不是 40
}

要在函数里保住长度,用引用绑定数组或直接传 std::span/容器:

cpp
void g(int (&arr)[10]) { cout << sizeof(arr); }  // 40

数组指针与指针数组 ​

靠 [] 和 * 的优先级区分。[] 优先级高于 *,所以要用括号改变结合。

cpp
int* a[10];     // 指针数组:10 个 int*
int (*b)[10];   // 数组指针:一个指针,指向含 10 个 int 的数组

int arr[10];
b = &arr;       // 注意是 &arr,不是 arr
cout << (*b)[3];

函数指针 ​

保存函数的入口地址,是回调和策略替换的基础机制。

cpp
bool cmp(int a, int b) { return a < b; }

bool (*pf)(int, int) = cmp;   // & 可省,函数名自动转指针
cout << pf(1, 2);             // 调用,也可写 (*pf)(1, 2)

声明写法同样受优先级影响:

cpp
bool (*pf)(int, int);   // pf 是函数指针
bool* pf(int, int);     // pf 是返回 bool* 的函数

用 using 起别名可读性好得多:

cpp
using Cmp = bool (*)(int, int);
void sortBy(vector<int>& v, Cmp c);

现代 C++ 里更常用 std::function 或模板参数,能吃 lambda 和带状态的可调用对象,函数指针只能接无捕获的。

void* 与类型擦除 ​

void* 能存任何对象的地址,但丢掉了类型,所以不能解引用、不能做算术,只能比较、传递、或转回原类型。

cpp
int a = 42;
void* pv = &a;
// *pv;                    // 错误:不知道该按什么类型读
int* pi = static_cast<int*>(pv);
cout << *pi;               // 42,转回正确类型才能用

转回的类型必须和原类型一致,转错是未定义行为。C 里用它做泛型(memcpy、qsort),C++ 里应优先用模板。

野指针与悬空指针 ​

两个常见的无效指针来源,都会导致解引用崩溃或更糟的静默数据损坏。

野指针是未初始化的指针,值是栈上的垃圾数据。悬空指针是曾经有效、但所指对象已销毁的指针:

cpp
int* dangling() {
    int local = 42;
    return &local;     // 返回局部变量地址,函数返回后栈帧失效
}

int* p = new int(1);
delete p;              // p 现在悬空
// *p = 2;             // 未定义行为
p = nullptr;           // 好习惯:delete 后立刻置空

delete 只释放内存,不改变指针的值,所以必须手动置空。置空后重复 delete nullptr 是安全的空操作,但重复 delete 同一个非空指针就是 double free。

智能指针 ​

手动 new/delete 容易漏、容易在异常路径上泄漏,现代 C++ 用 RAII 把释放交给析构函数。

  • unique_ptr:独占所有权,不可拷贝只可移动,零额外开销,默认选它
  • shared_ptr:引用计数共享,计数归零才释放,有控制块开销
  • weak_ptr:不增加计数的观察者,用来打破 shared_ptr 循环引用
cpp
auto p = make_unique<int>(42);       // 独占
auto s = make_shared<vector<int>>(); // 共享
weak_ptr<vector<int>> w = s;         // 观察,不延长生命周期
if (auto locked = w.lock()) {        // 用前提升为 shared_ptr
    locked->push_back(1);
}

两个 shared_ptr 互相持有对方,计数永远降不到 0,内存泄漏,把其中一端改成 weak_ptr 即可。

常见追问 ​

指针大小为什么与类型无关:指针存的是地址,地址宽度由机器决定。类型只影响编译期如何解释这块内存和算术步长。

delete 和 delete[] 能混用吗:不能。new[] 分配的内存需要 delete[] 才会逐个调析构,混用是未定义行为。

int* p, q; 声明了什么:p 是 int*,q 是 int。* 属于声明符而非类型名,这也是建议一行一个声明的原因。

为什么形参改指针指向不影响调用方:指针是按值传递的,函数拿到的是地址的副本。要改调用方的指针本身,得传 int** 或 int*&。

cpp
void bad(int* p)  { p = new int(1); }   // 调用方的指针没变,还泄漏了
void good(int*& p) { p = new int(1); }  // 有效

C++ 指针:占多大空间,有哪些常见用法 ​

指针本质上是一个变量,它保存的不是数据本身,而是另一个对象在内存中的地址。通过这个地址,程序可以间接读写目标对象。

指针占多大 ​

指针的大小只取决于平台的地址宽度,和它指向什么类型无关。64 位系统下所有指针都是 8 字节,32 位系统下是 4 字节。

cpp
#include <iostream>

int main() {
    double *pd = nullptr;
    short  *ps = nullptr;
    std::cout << sizeof(pd) << " " << sizeof(ps) << std::endl; // 64 位下输出:8 8
}

指针类型的作用是告诉编译器:解引用时按多大读取数据,p + 1 时向前移动多少字节。

常见用法 ​

1. 指向动态分配的对象

cpp
struct Node { int val; };
Node *node = new Node{42};
delete node;  // 用完要释放,否则内存泄漏

2. 指向常量

cpp
const int limit = 100;
const int *pl = &limit;  // 不能通过 pl 修改 limit,但 pl 可以改指向

3. 函数指针

cpp
int multiply(int x, int y) { return x * y; }

int (*op)(int, int) = multiply;
std::cout << op(3, 5) << std::endl;  // 15

常用于回调和策略切换。现代 C++ 中也可以用 std::function 或 lambda 代替。

4. 指向类成员

成员变量可以直接取地址;成员函数指针要带上类名,调用时必须绑定具体对象。

cpp
class Counter {
public:
    int a = 2, b = 3;
    int sum() { return a + b; }
};

Counter c;
int *pa = &c.a;                    // 指向成员变量
int (Counter::*fp)() = &Counter::sum;  // 成员函数指针,& 不能省
std::cout << (c.*fp)() << std::endl;   // 5

5. this 指针

每个非静态成员函数都隐含一个 this,指向调用它的对象。它是指针常量,不能改指向。常用于区分同名的参数和成员,或者返回 *this 实现链式调用:

cpp
class Builder {
    int width = 0;
public:
    Builder& setWidth(int width) {
        this->width = width;
        return *this;
    }
};

使用时要注意 ​

  • 定义时就初始化,暂时没有目标就用 nullptr,避免野指针。
  • delete 之后把指针置空,防止悬空指针被再次使用。
  • 能用引用或智能指针(unique_ptr、shared_ptr)时,优先用它们管理资源。

总结 ​

指针保存的是地址,大小由平台决定。常见用法包括指向堆对象、常量、函数、类成员,以及成员函数内部隐含的 this。实际开发中尽量用智能指针管理生命周期,减少手动 new/delete。


📮 联系方式

微信:BuZhiCoder | QQ:1776418445 扫码添加,备注「求职」优先通过

微信二维码

基于 VitePress 搭建

🌟训练营🤖AI面试📄做简历💻刷题