全局变量、局部变量、静态局部变量、静态全局变量
全局变量在一个文件中声明,但是可以作用于所有文件,当然其他文件需要加extern 来声明 局部变量,具有局部作用域,然后在函数生命周期内有效 静态局部变量,具有局部作用域,但是第一次定义后整个程序期间都有效,但对其他函数不可见,适合函数运行在程序生命周期的变量,比如状态机 静态全局变量,用来保证该变量对其他文件是不可见的,只在定义它的文件时可见的,即使函数重复定
const 作用和用法
修饰变量表示这个变量是不可以修改的 修饰函数参数表示这个函数不可以修改这个传递进来的参数 修改成员函数表示这个函数不可以修改其他成员变量的值
C++程序崩溃会core dump一个栈,这个栈的内容是什么
函数调用链:从崩溃的函数开始,一层层的往上列所有调用的函数 每个函数的地址:记录每个函数在内存的具体地址,结合编译的符号表,可以对应具体那个函数 函数的参数和局部变量:会保存这个函数传入的参数值,以及函数内部定义的临时变量 栈指针的位置:记录崩溃时栈顶的位置,告诉你当前栈用到了哪里,有没有溢出
拷贝构造函数
第一个参数是自身类类型的引用(通常是 const 引用),其余参数都有默认值。因参数需引用,否则传参会无限递归。不写则编译器合成一个,逐成员拷贝。
拷贝初始化:T a = b; 传非引用参数、返回非引用值时都会触发,靠拷贝构造完成。explicit 修饰的函数禁止隐式转换。
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 成员:类类型调其拷贝构造,内置类型直接拷贝,数组逐元素拷贝。某些类的合成版会被定义为删除以禁止拷贝。就是默认拷贝构造函数。
// 等价于 Sales_data 的合成拷贝构造
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), // string 的拷贝构造
units_sold(orig.units_sold), // 直接拷贝
revenue(orig.revenue) { } // 直接拷贝浅拷贝 合成拷贝对指针成员只复制指针值,两对象指向同一块内存。析构时会 double free,一方修改也会影响另一方。
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 崩溃深拷贝
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 字节。
int a = 42;
int* p = &a; // & 取地址,p 保存 a 的地址
cout << *p; // * 解引用,读出 42
*p = 100; // 通过指针写,a 变成 100
cout << sizeof(p); // 8(64 位)
cout << sizeof(double*); // 8,指向什么都一样指针的类型决定解引用时"按什么方式解释这块内存"以及指针算术的步长,这是编译期的事,不影响指针自身的大小。
指针的四种状态
任意时刻一个指针处于四种状态之一:
- 指向一个对象
- 指向对象的下一个位置(尾后指针,如
arr + n,可比较不可解引用) - 空指针,不指向任何对象
- 无效指针,以上都不是
解引用后两种是未定义行为。空指针至少还能判断,无效指针连判断都做不到,所以定义指针时就该初始化。
int* p1 = nullptr; // C++11 推荐
int* p2 = NULL; // 老写法,本质是宏 0
int* p3 = 0; // 合法但不建议
int* p4; // 危险:值是随机的,野指针nullptr 优于 NULL,因为 NULL 就是整数 0,碰上重载会选错:
void f(int);
void f(char*);
f(NULL); // 调 f(int),不是想要的那个
f(nullptr); // 调 f(char*)指针与引用的区别
面试高频。核心:引用是别名,指针是对象。
| 指针 | 引用 | |
|---|---|---|
| 是否是对象 | 是,有自己的地址 | 不是,只是别名 |
| 必须初始化 | 否(但应该) | 是 |
| 能否重新绑定 | 能,随时改指向 | 不能,从生到死只绑一个对象 |
| 能否为空 | 能,nullptr | 不能 |
| 多级 | 有 int** | 没有引用的引用 |
sizeof | 指针大小(8) | 被引对象的大小 |
int a = 1, b = 2;
int* p = &a; p = &b; // OK,改指向
int& r = a; r = b; // 不是重新绑定,是把 b 的值赋给 a引用本质是编译器帮你做的自动解引用,多数实现底层也用指针,但语言层面它没有独立身份。
const 和指针
const 位置不同含义完全不同,看 * 的左右:* 左边修饰所指的对象,右边修饰指针本身。
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) 的原因。
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。跨数组做算术是未定义行为。
数组退化也解释了为什么函数参数里的数组拿不到长度:
void f(int arr[10]) { // 等价于 void f(int* arr)
cout << sizeof(arr); // 8,不是 40
}要在函数里保住长度,用引用绑定数组或直接传 std::span/容器:
void g(int (&arr)[10]) { cout << sizeof(arr); } // 40数组指针与指针数组
靠 [] 和 * 的优先级区分。[] 优先级高于 *,所以要用括号改变结合。
int* a[10]; // 指针数组:10 个 int*
int (*b)[10]; // 数组指针:一个指针,指向含 10 个 int 的数组
int arr[10];
b = &arr; // 注意是 &arr,不是 arr
cout << (*b)[3];函数指针
保存函数的入口地址,是回调和策略替换的基础机制。
bool cmp(int a, int b) { return a < b; }
bool (*pf)(int, int) = cmp; // & 可省,函数名自动转指针
cout << pf(1, 2); // 调用,也可写 (*pf)(1, 2)声明写法同样受优先级影响:
bool (*pf)(int, int); // pf 是函数指针
bool* pf(int, int); // pf 是返回 bool* 的函数用 using 起别名可读性好得多:
using Cmp = bool (*)(int, int);
void sortBy(vector<int>& v, Cmp c);现代 C++ 里更常用 std::function 或模板参数,能吃 lambda 和带状态的可调用对象,函数指针只能接无捕获的。
void* 与类型擦除
void* 能存任何对象的地址,但丢掉了类型,所以不能解引用、不能做算术,只能比较、传递、或转回原类型。
int a = 42;
void* pv = &a;
// *pv; // 错误:不知道该按什么类型读
int* pi = static_cast<int*>(pv);
cout << *pi; // 42,转回正确类型才能用转回的类型必须和原类型一致,转错是未定义行为。C 里用它做泛型(memcpy、qsort),C++ 里应优先用模板。
野指针与悬空指针
两个常见的无效指针来源,都会导致解引用崩溃或更糟的静默数据损坏。
野指针是未初始化的指针,值是栈上的垃圾数据。悬空指针是曾经有效、但所指对象已销毁的指针:
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循环引用
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*&。
void bad(int* p) { p = new int(1); } // 调用方的指针没变,还泄漏了
void good(int*& p) { p = new int(1); } // 有效C++ 指针:占多大空间,有哪些常见用法
指针本质上是一个变量,它保存的不是数据本身,而是另一个对象在内存中的地址。通过这个地址,程序可以间接读写目标对象。
指针占多大
指针的大小只取决于平台的地址宽度,和它指向什么类型无关。64 位系统下所有指针都是 8 字节,32 位系统下是 4 字节。
#include <iostream>
int main() {
double *pd = nullptr;
short *ps = nullptr;
std::cout << sizeof(pd) << " " << sizeof(ps) << std::endl; // 64 位下输出:8 8
}指针类型的作用是告诉编译器:解引用时按多大读取数据,p + 1 时向前移动多少字节。
常见用法
1. 指向动态分配的对象
struct Node { int val; };
Node *node = new Node{42};
delete node; // 用完要释放,否则内存泄漏2. 指向常量
const int limit = 100;
const int *pl = &limit; // 不能通过 pl 修改 limit,但 pl 可以改指向3. 函数指针
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. 指向类成员
成员变量可以直接取地址;成员函数指针要带上类名,调用时必须绑定具体对象。
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; // 55. this 指针
每个非静态成员函数都隐含一个 this,指向调用它的对象。它是指针常量,不能改指向。常用于区分同名的参数和成员,或者返回 *this 实现链式调用:
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 扫码添加,备注「求职」优先通过
