Learning C++ (Day 1)
1. 指针与引用
| 对比项 | 指针 T* |
引用 T& |
|---|---|---|
| 可否为空 | 可以,nullptr 合法 |
必须绑定对象,无空引用 |
| 能否重新绑定 | 可以指向别的对象 | 绑定后终身不变 |
| 是否占内存 | 占 8 字节(64 位系统) | 语义上是别名,通常不占;作类成员或数组元素时占位 |
| 算术运算 | 支持 p+1、p[i] |
不支持 |
| 多级 | T** 合法 |
无引用的引用,T&& 折叠为 T& |
| 典型用途 | 可选参数、资源句柄、动态数组 | 函数参数与返回值,避免拷贝 |
2. const 的位置:从右往左念
const int* p; // 指向常量的指针:*p 不可改,p 可改
int* const p = &a; // 常量指针:p 不可改,*p 可改
const int* const p; // 两者都不可改
int f() const; // 常量成员函数:不允许修改成员变量(mutable 除外)
详细解析:
const int* p:不能改变指针p指向的int变量的值,可以改变p指向的int变量对象。int* const p:可以改变指针p指向的int变量的值,不能改变p指向的int变量对象。const int* const p:二者均不可改变。int f() const:不能改变成员变量。
代码示例:
class Animal {
int age = 5;
public:
int getAge() const {
// age = 10; // ❌ 编译错误:const 函数里不能改成员
return age; // ✅ 只能读
}
void setAge(int a) { // 无 const
age = a; // ✅ 能改成员
}
};
int main() {
const Animal a;
a.getAge(); // ✅ const 对象可以调 const 函数
a.setAge(10); // ❌ const 对象不能调非 const 函数
}
3. 初始化:优先用花括号
int a{3.5}; // ❌ 编译报错:禁止窄化转换
Widget w(); // ⚠️ 陷阱!声明了一个返回 Widget 的函数
Widget w{}; // ✅ 正确:调用默认构造
std::vector<int> v{10}; // 1 个元素,值为 10
std::vector<int> v2(10); // 10 个元素,值为 0
4. auto 与类型推导
const int& r = a;
auto x1 = r; // int —— 丢掉顶层 const,且按值拷贝
auto& x2 = r; // const int& —— 保留引用与 const
auto&& x3 = f(); // 万能引用 —— 能绑定左值也能绑定右值
decltype(r) x4 = a; // 完整保留类型:const int&
关于万能引用 auto&&:
auto 待推导,&& 跟着它,导致它能绑左值也能绑右值。背后靠引用折叠:左值推导成 &,右值推导成 &&。
auto&& 作用之一:移动构造
class MyString {
char* data;
public:
MyString(const char* s) { /* 构造 */ }
// 拷贝构造:参数 const MyString&
MyString(const MyString& other) {
cout << "拷贝构造\n";
// 深拷贝...
}
// 移动构造:参数 MyString&&
MyString(MyString&& other) noexcept {
cout << "移动构造\n";
data = other.data;
other.data = nullptr;
}
};
int main() {
MyString a = "hello";
MyString b = a; // a 是左值 → 触发拷贝构造
MyString c = MyString("hi"); // 临时对象是右值 → 触发移动构造
}
编译器优化 (RVO/NRVO):
vector<int> makeVec() {
vector<int> v(1000000); // 造一个百万元素的 vector
return v;
}
vector<int> a = makeVec();
makeVec() 返回的是临时对象(右值),会自动匹配 vector 的移动构造函数,把内部指针“偷”给 a。
5. 值类别与移动语义
std::string a = "hello";
std::string b = a; // 拷贝
std::string c = std::move(a); // 移动:a 处于有效但未指定状态,不要再依赖其内容
- 左值:有名字、可取地址;右值:临时量、字面量。
std::move本身不移动任何东西,只是把实参强转成右值引用,真正的转移发生在移动构造里。- 注意:
c = std::move(a)之后,a为空。 - 避坑:返回局部对象不要写
return std::move(local);,会阻止 NRVO(命名返回值优化),反而更慢。
6. 面向对象三大特性
- 封装:把数据与操作数据的方法绑在一起,用访问限定符(
public/protected/private)划定边界。 - 继承:复用与扩展,注意 "is-a" 关系,避免为了复用而继承。
- 多态:同一接口、不同实现。分编译期多态(重载、模板)与运行期多态(虚函数)。
7. 运行期多态的底层机制
每个含虚函数的类有一张虚函数表 vtable(存函数指针),每个对象里有一个虚表指针 vptr。调用 p->g() 的路径是:读对象 vptr → 找到实际类型的 vtable → 取对应槽位 → 调用派生类实现。
struct Base {
virtual void f();
virtual void g();
virtual ~Base(); // 有虚函数就配虚析构
};
struct Derived : Base {
void g() override; // 覆盖,务必写 override
};
8. 虚析构:不写就泄漏
Base* p = new Derived();
delete p;
// 若 ~Base 非虚,只调用 ~Base,Derived 独有的资源全部泄漏
9. 重载 / 重写 / 隐藏
| 名称 | 发生位置 | 条件 | 绑定时机 |
|---|---|---|---|
| 重载 (Overload) | 同一作用域 | 同名但参数列表不同 | 编译期 |
| 重写 (Override) | 基类与派生类 | 虚函数,签名完全一致 | 运行期 |
| 隐藏 (Hide) | 基类与派生类 | 同名即遮蔽(不论签名) | 编译期 |
隐藏的解决与代码示例:
struct Base { void f(int); };
struct Derived : Base {
void f(double); // 隐藏了 Base::f(int)
using Base::f; // 用 using 把基类版本拉回来,解除隐藏
};
10. 构造与析构顺序
核心原则:构造:基类 → 成员(按声明顺序,不是初始化列表顺序)→ 自身函数体。析构完全相反。
易错点:构造函数不能是虚的(此时vptr还没初始化完);在构造/析构函数中调用虚函数,走的是当前类版本,不会发生多态。
代码示例:
struct MemberA {
MemberA() { std::cout << "MemberA\n"; }
~MemberA() { std::cout << "~MemberA\n"; }
};
struct MemberB {
MemberB() { std::cout << "MemberB\n"; }
~MemberB() { std::cout << "~MemberB\n"; }
};
struct Base1 {
Base1() { std::cout << "Base1\n"; }
~Base1() { std::cout << "~Base1\n"; }
};
struct Base2 {
Base2() { std::cout << "Base2\n"; }
~Base2() { std::cout << "~Base2\n"; }
};
class Derived : public Base2, public Base1 { // 继承声明:Base2 在前,Base1 在后
private:
MemberB mb; // 成员声明:mb 在前,ma 在后
MemberA ma;
public:
// 陷阱:初始化列表即使把 Base1/ma 写在前面,编译器也完全忽略该书写顺序!
Derived() : Base1(), Base2(), ma(), mb() {
std::cout << "Derived Body\n";
}
~Derived() {
std::cout << "~Derived Body\n";
}
};
void solve() {
Derived d;
}
执行输出解析:
[构造]
Base2 <-- 1. 按基类继承声明顺序(Base2 在前)
Base1 <-- 2. 第二基类
MemberB <-- 3. 按类内成员声明顺序(mb 在前)
MemberA <-- 4. 第二成员
Derived Body <-- 5. 自身构造函数体
[析构]
~Derived Body <-- 1. 自身析构函数体
~MemberA <-- 2. 成员逆序(ma 先于 mb)
~MemberB <-- 3.
~Base1 <-- 4. 基类逆序(Base1 先于 Base2)
~Base2 <-- 5.
深入总结执行顺序
【构造顺序】(由底层向顶层推进)
- 虚基类(Virtual Base Classes)
- 按在派生体系中声明的拓扑顺序,由最底层派生类统一构造(仅构造一次)
- 普通基类(Direct Base Classes)
- 按类定义冒号后的【继承声明顺序】从左至右构造(与初始化列表书写顺序无关)
- 非静态成员对象(Member Objects)
- 按类体内的【变量声明顺序】自上而下构造(与初始化列表书写顺序无关)
- 当前类构造函数体(Constructor Body
{ ... })
【析构顺序】(严格镜像逆序,剥洋葱模型)
- 当前类析构函数体
- 非静态成员对象(按声明顺序的逆序逐个析构)
- 普通基类(按继承声明顺序的逆序逐个析构)
- 虚基类(按虚基类构造顺序的逆序最终析构)
11. 对象切片 (Object Slicing)
class Animal {
public:
virtual void speak() { cout << "动物叫" << endl; }
};
class Dog : public Animal {
public:
void speak() override { cout << "汪汪汪" << endl; }
void wagTail() { cout << "摇尾巴" << endl; } // Dog 特有
};
int main() {
Dog dog;
Animal a = dog; // ⚠️ 对象切片!Dog 特有的部分被切掉
a.speak(); // 输出:动物叫(不是汪汪汪!)
// a.wagTail(); // ❌ 编译错误:a 是 Animal,没有 wagTail
}
结论:多态必须通过指针或引用传递,按值传递会导致派生类特征丢失(切片)。
12. 三法则 / 五法则
只要手动写了析构、拷贝构造、拷贝赋值中的任意一个,就要一并考虑移动构造与移动赋值,否则容易出现浅拷贝、双次释放或性能退化。
class Buffer {
char* data_;
size_t n_;
public:
Buffer(size_t n) : data_(new char[n]), n_(n) {}
~Buffer() { delete[] data_; }
// 拷贝构造(深拷贝)
Buffer(const Buffer& o) : data_(new char[o.n_]), n_(o.n_) {
std::memcpy(data_, o.data_, n_);
}
// 拷贝赋值 (Copy-and-Swap 惯用法)
Buffer& operator=(Buffer o) {
swap(o);
return *this;
}
// 移动构造
Buffer(Buffer&& o) noexcept : data_(o.data_), n_(o.n_) {
o.data_ = nullptr;
o.n_ = 0;
}
// 工具函数
void swap(Buffer& o) noexcept {
std::swap(data_, o.data_);
std::swap(n_, o.n_);
}
};

浙公网安备 33010602011771号