乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

构造函数与析构函数

定义与作用

构造函数在对象创建时自动调用,负责初始化对象。析构函数在对象生命周期结束时自动调用,负责释放资源。

class File {
public:
    File(const std::string& path);  // 构造函数:打开文件
    ~File();                        // 析构函数:关闭文件
};

构造函数和析构函数是 RAII(资源获取即初始化)的基础——将资源生命周期绑定到对象生命周期。

核心原理

构造函数分类与调用时机

委托构造函数(C++11)

委托构造函数让一个构造函数调用同类的另一个构造函数,消除重复代码。

完整示例

示例一:课程管理系统的构造与析构

场景说明:设计 Course 和 Enrollment 类,展示初始化列表、委托构造和析构顺序。

#include <iostream>
#include <string>
#include <vector>

class Course {
public:
    // 默认构造函数
    Course() : Course("未命名", 0, 0.0) {
        std::cout << "  [默认构造] " << name_ << std::endl;
    }

    // 主构造函数
    Course(std::string name, int credit, double hours)
        : name_(std::move(name)), credit_(credit), hours_(hours) {
        std::cout << "  [主构造] " << name_
                  << " (学分: " << credit_ << ", 课时: " << hours_ << ")" << std::endl;
        validate();
    }

    // 委托构造函数:只需名称,其他用默认值
    Course(std::string name) : Course(std::move(name), 3, 48.0) {}

    // 析构函数
    ~Course() {
        std::cout << "  [析构] " << name_ << std::endl;
    }

    const std::string& getName() const { return name_; }

private:
    void validate() const {
        if (name_.empty()) {
            throw std::invalid_argument("课程名称不能为空");
        }
    }

    std::string name_;
    int credit_;
    double hours_;
};

class Enrollment {
public:
    Enrollment(const std::string& student, const std::string& course)
        : student_(student), course_(course) {
        std::cout << "[选课] " << student_ << " 选了 " << course_.getName() << std::endl;
    }

    ~Enrollment() {
        std::cout << "[退课] " << student_ << " 退出 " << course_.getName() << std::endl;
    }

private:
    std::string student_;
    Course course_;  // 成员对象,构造/析构顺序由声明顺序决定
};

int main() {
    std::cout << "===== 构造函数演示 =====" << std::endl;

    {
        std::cout << "\n-- 创建 Course 对象 --" << std::endl;
        Course c1("高等数学", 5, 64.0);
        Course c2("线性代数");           // 委托构造
        Course c3;                      // 默认构造

        std::cout << "\n-- 作用域结束,对象销毁 --" << std::endl;
    }  // c3, c2, c1 逆序析构

    std::cout << "\n===== 析构顺序 =====" << std::endl;
    {
        Enrollment e("张三", "数据结构");
        // e 析构时先析构 course_,再析构 student_
    }

    return 0;
}

预期输出:

===== 构造函数演示 =====

-- 创建 Course 对象 --
  [主构造] 高等数学 (学分: 5, 课时: 64)
  [主构造] 线性代数 (学分: 3, 课时: 48)
  [主构造] 未命名 (学分: 0, 课时: 0)
  [默认构造] 未命名

-- 作用域结束,对象销毁 --
  [析构] 未命名
  [析构] 线性代数
  [析构] 高等数学

===== 析构顺序 =====
[选课] 张三 选了 数据结构
  [主构造] 数据结构 (学分: 3, 课时: 48)
[退课] 张三 退出 数据结构
  [析构] 数据结构

逐段分析:

  • 初始化列表 : name_(std::move(name)), credit_(credit), hours_(hours) 在进入构造函数体之前完成初始化——效率高于体内赋值
  • 委托构造:Course(string) 委托给 Course(string, int, double),所有初始化逻辑集中在一个主构造函数中
  • 默认构造通过委托构造调用主构造,保证所有路径都经过同一初始化流程
  • 析构顺序:逆构造顺序——先构造的后析构。成员对象按其声明顺序逆序析构
  • Enrollment 包含 Course 成员对象,析构时先析构 course_ 再析构 student_(声明顺序的逆序)

示例二:转换构造函数与 explicit

场景说明:演示隐式转换的风险和 explicit 的保护作用。

#include <iostream>
#include <string>

class PhoneNumber {
public:
    // 转换构造函数:string → PhoneNumber
    explicit PhoneNumber(const std::string& number) : number_(number) {
        std::cout << "  构造 PhoneNumber: " << number_ << std::endl;
    }

    const std::string& get() const { return number_; }

private:
    std::string number_;
};

class Contact {
public:
    Contact(const std::string& name, const PhoneNumber& phone)
        : name_(name), phone_(phone) {
        std::cout << "  创建联系人: " << name_ << " | "
                  << phone_.get() << std::endl;
    }

private:
    std::string name_;
    PhoneNumber phone_;
};

// 无 explicit 的危险版本
class BadPhoneNumber {
public:
    BadPhoneNumber(const std::string& number) : number_(number) {}

private:
    std::string number_;
};

void dial(const BadPhoneNumber& phone) {
    std::cout << "拨号中..." << std::endl;
}

int main() {
    std::cout << "===== explicit 保护 =====" << std::endl;

    // 必须显式构造 PhoneNumber
    Contact alice("张三", PhoneNumber("13800138000"));

    // 下面这行编译错误(explicit 阻止了隐式转换)
    // Contact bob("李四", "13900139000");

    // 没有 explicit 的话,下面的调用会悄然创建一个 BadPhoneNumber
    dial(std::string("110"));  // 隐式转换:string → BadPhoneNumber → dial

    // 对比:explicit 要求显式写出转换
    // PhoneNumber pn = "13800138000";  // 编译错误(explicit)

    return 0;
}

预期输出:

===== explicit 保护 =====
  构造 PhoneNumber: 13800138000
  创建联系人: 张三 | 13800138000
拨号中...

逐段分析:

  • explicit PhoneNumber(const string&) 阻止了隐式类型转换,避免 Contact("张三", "138...") 中字符串被悄然转为 PhoneNumber
  • 无 explicit 的 BadPhoneNumber 允许 dial(string("110")) 中字符串隐式转换为 BadPhoneNumber——这种行为可能出乎意料
  • 单参数构造函数默认应标记为 explicit,除非确实需要隐式转换
  • explicit 只对单参数构造函数和转换运算符有效

易错场景与面试考点

易错场景

场景错误表现正确做法
初始化列表顺序与成员声明顺序不一致未定义行为初始化列表按成员声明顺序写
在构造函数体中使用未在初始化列表中初始化的成员使用未初始化的值所有成员在初始化列表中初始化
单参数构造函数未加 explicit意外的隐式类型转换默认加 explicit
虚构造函数C++ 没有虚构造函数用工厂模式代替
析构函数中抛异常栈展开中双重异常 → terminate析构函数不应抛异常

常见面试问题

  1. 初始化列表和构造函数体内赋值的区别?——初始化列表是真正的初始化(调用成员的构造函数),体内赋值是先默认初始化再赋值。对 const 成员和引用成员,必须用初始化列表。

  2. 什么是委托构造函数?什么场景使用?——C++11,一个构造函数调用同类另一个构造函数。用于消除多个构造函数间的重复初始化代码。

  3. 为什么析构函数通常应是虚函数?——当通过基类指针 delete 派生类对象时,如果基类析构函数不是虚函数,只会调用基类析构函数,派生类资源泄漏。

  4. explicit 关键字的作用?什么时候使用?——阻止隐式类型转换。单参数构造函数和转换运算符默认应加 explicit。

  5. 析构顺序是怎样的?——派生类析构 → 成员析构(声明逆序) → 基类析构。构造顺序相反。

小结

  • 初始化列表优于构造函数体内赋值——真正的初始化 vs 先默认再赋值
  • 委托构造函数(C++11)消除重复代码,集中管理初始化逻辑
  • 单参数构造函数默认加 explicit,防止隐式类型转换
  • 析构函数不应抛异常,含虚函数的基类析构函数应为 virtual
  • 成员初始化顺序由声明顺序决定,初始化列表顺序应与之一致
上一页
类的基本定义
下一页
拷贝控制