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 除外)

详细解析:

  1. const int* p:不能改变指针 p 指向的 int 变量的值,可以改变 p 指向的 int 变量对象。
  2. int* const p:可以改变指针 p 指向的 int 变量的值,不能改变 p 指向的 int 变量对象。
  3. const int* const p:二者均不可改变。
  4. 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.

深入总结执行顺序

【构造顺序】(由底层向顶层推进)

  1. 虚基类(Virtual Base Classes)
    • 按在派生体系中声明的拓扑顺序,由最底层派生类统一构造(仅构造一次)
  2. 普通基类(Direct Base Classes)
    • 按类定义冒号后的【继承声明顺序】从左至右构造(与初始化列表书写顺序无关)
  3. 非静态成员对象(Member Objects)
    • 按类体内的【变量声明顺序】自上而下构造(与初始化列表书写顺序无关)
  4. 当前类构造函数体(Constructor Body { ... })

【析构顺序】(严格镜像逆序,剥洋葱模型)

  1. 当前类析构函数体
  2. 非静态成员对象(按声明顺序的逆序逐个析构)
  3. 普通基类(按继承声明顺序的逆序逐个析构)
  4. 虚基类(按虚基类构造顺序的逆序最终析构)

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_); 
    }
};
posted @ 2026-10-08 18:06  カガリ  阅读(7)  评论(0)    收藏  举报