Day 1

C++ 对 C 的扩展  +  类和对象基础

Qt 方向实训 · 第 1 / 4 天 · 讲:练 ≈ 1:2.7

  • 模块 A(3 课时):C++ 对 C 的扩展 —— 强化,非入门
  • 模块 B(3 课时):类和对象基础

大纲:docs/teaching/OUTLINE.md

模块 A · 讲授 35min

C++ 对 C 的扩展

已默认你熟悉这些语法——今天讲「为什么」,不讲「怎么写」:

  • namespace 的作用域本质
  • 函数重载的决议机制
  • const 的本质 vs #define
  • 引用的本质(语法糖视角)
  • 三目运算符可否作左值 —— 纯版本差异

强化点 1 / 5

namespace 为什么不能定义在函数体内


namespace A{
	int a = 10;
}
namespace B{
	int a = 20;
}
void test(){
	cout << "A::a : " << A::a << endl;
	cout << "B::a : " << B::a << endl;
}
    

作用域是「文件级」概念,不是「语句顺序」——wiki 原文把 namespace 写进了函数体内,编译不过;出处:p01/ch03/namespace_syntax.cpp, 修正记录 ERRATA.md [58954439:3/6/7]

强化点 2 / 5

函数重载:二义性从哪来


void MyFunc(string b){ cout << "b: " << b << endl; }
// 函数重载碰上默认参数
void MyFunc(string b, int a = 10){ cout << "a: " << a << " b:" << b << endl; }

int main(){
    // MyFunc("hello");        // 二义:两个重载同等匹配
    MyFunc("hello", 20);       // 显式给第二参才能消歧义
}
    

重载决议只看「能否唯一确定最佳匹配」,默认参数不参与决议—— 出处:p01/ch03/function_overload_basics_2.cppERRATA.md [58954439:42] ambiguous-overload

强化点 3 / 5

const 的本质 vs #define


const int constA = 10;
int main(){
    int* p = (int*)&constA;
    *p = 200;          // 绕过 const 的“建议性”检查——编译期常量 ≠ 只读内存
}
    

const 是「编译期类型检查」,不是「运行期内存保护」;#define 只是文本替换, 没有类型、没有作用域。出处:p01/ch03/const_diff_summary.cpp

强化点 4 / 5

引用的本质


void testFunc(int& ref){
    ref = 100;  // 编译器内部转换为 *ref = 100
}
int main(){
    int a = 10;
    int& aRef = a;   // 自动转换为 int* const aRef = &a —— 这也是引用必须初始化的原因
    aRef = 20;       // 内部转换为 *aRef = 20
}
    

把引用当成「自动解引用的 const 指针」来理解,很多“为什么引用不能重新绑定” 的疑惑会自然消失。出处:p01/ch03/reference_essence.cpp

强化点 5 / 5 · 版本差异

三目运算符能否作左值 —— 纯版本问题


// C(p01/ch03/ternary_operator.c):三目表达式返回右值
(a > b ? a : b) = 100;   // C 中非法

// C++(p01/ch03/ternary_operator.cpp):三目表达式返回左值引用
(a > b ? a : b) = 100;   // C++ 中合法,b 会被改写
    

现场演示:tools/std_probe.py --lang c --file p01/ch03/ternary_operator.c --standards c89,c99,c11,c17 --pedantic-errors —— 对照 docs/VERSION_NOTES.md §2

模块 A · 练习 95min

动手:ch03 强化练习

目录:p01/ch03/ (47 个源文件,本节任务只挑一部分,其余留作参考自学)

任务卡 1 · 逐一验证

p01/ch03 挑 8–10 个文件(namespace / reference / overload / const 系列),逐个执行:

tools/run.sh p1c03_03   # namespace_syntax
tools/run.sh p1c03_33   # reference_essence
tools/run.sh p1c03_43   # function_overload_basics_2
# ……自行在 build/ 里用 ninja -t targets 或 cmake --build 找到其余目标名

对照 docs/ERRATA.md 对应条目,写一句话解释 「wiki 原文错在哪、现在为什么对」。

任务卡 2 · 版本探针实操

tools/std_probe.py --lang c --file p01/ch03/ternary_operator.c \
    --standards c89,c99,c11,c17 --pedantic-errors

记录哪个标准报错、哪个不报错,对照 docs/VERSION_NOTES.md §2 的结论,理解“同一段代码 在不同标准下含义不同”。

任务卡 3 · 拔高(选做)

阅读 docs/ERRATA.md [58954439:42],解释生成脚本的 去重逻辑为什么会把重载函数改名导致二义性——这是一次「工具误伤正确代码」的真实案例, 训练读 diff、读生成器逻辑的能力。

参考:tools/gen_part1.pydedupe 相关逻辑

模块 B · 讲授 35min

类和对象基础

  • 封装:C 结构体 vs C++ 类
  • 构造 / 析构的调用时机
  • 深拷贝 vs 浅拷贝
  • 静态成员与单例

封装:同一个问题,两种语言写法


// C:结构体没有访问控制,甚至类型都保护不了
typedef struct _Person{ char name[64]; int age; } Person;
typedef struct _Aninal{ char name[64]; int age; int type; } Ainmal;

void AnimalEat(Ainmal* animal){ printf("%s在吃动物吃的饭!\n", animal->name); }

int main(){
    Person person; strcpy(person.name, "小明"); person.age = 30;
    AnimalEat(&person);   // 把 Person* 传给期望 Ainmal* 的函数——编译器毫无察觉
}
    

对照:class_encapsulation.c vs class_encapsulation.cpp (public/protected/private 三级访问控制)

构造 / 析构的调用时机


class Person{
public:
    Person(){
        cout << "构造函数调用!" << endl;
        pName = (char*)malloc(sizeof("John"));
        strcpy(pName, "John");
    }
    ~Person(){
        cout << "析构函数调用!" << endl;
        if (pName != NULL){ free(pName); pName = NULL; }
    }
private:
    char* pName;
};
    

强化点:wiki 原文这一串例子普遍漏写 #include <cstring> (用到 strcpy 却不声明)——出处 p01/ch04/s03/ctor_and_dtor.cppERRATA.md [58954451:0..13] missing-include:编译器不报错, 但换个平台/换个头文件包含顺序就可能炸——这类隐式依赖正是“强化”该抓的细节。

深拷贝 vs 浅拷贝


class Person{
public:
    Person(char* name, int age){
        pName = (char*)malloc(strlen(name) + 1);
        strcpy(pName, name);
    }
    // 自己实现拷贝构造函数 —— 深拷贝
    Person(const Person& person){
        pName = (char*)malloc(strlen(person.pName) + 1);
        strcpy(pName, person.pName);
    }
    ~Person(){ if (pName != NULL) free(pName); }
private:
    char* pName;
};
void test(){
    Person p1("Edward", 30);
    Person p2 = p1;   // 不写拷贝构造 → 默认浅拷贝 → 两个指针指向同一块内存 → 析构两次 double free
}
    

出处:p01/ch04/s03/deep_vs_shallow_copy.cpp

静态成员与单例


class Printer{
public:
    static Printer* getInstance(){ return pPrinter; }
    void PrintText(string text){ cout << text << endl; mTimes++; }
private:
    Printer(){ mTimes = 0; }
    Printer(const Printer&){}   // 禁止拷贝
private:
    static Printer* pPrinter;
    int mTimes;
};
Printer* Printer::pPrinter = new Printer;   // 类外初始化静态成员
    

出处:p01/ch04/s03/static_singleton.cpp

模块 B · 练习 95min

动手:类和对象

任务卡 1 · 扩展一个类

point_and_circle.cppcube_class_design.cpp 为起点, 新增一个成员函数(如计算面积 / 体积),编译运行验证输出正确。

任务卡 2 · 复现并修复浅拷贝崩溃

修改 deep_vs_shallow_copy.cpp: 故意删掉拷贝构造函数触发浅拷贝,用 tools/run.sh 观察 double free / 崩溃或异常输出,再恢复深拷贝对比。

任务卡 3 · 独立实现单例

参照 static_singleton.cpp, 独立写一个单例类(拷贝构造设为私有 / delete,验证多次 getInstance() 返回同一地址)。

当日产出验证

验收标准

cmake --build build --target all_p01_ch03
cmake --build build --target all_p01_ch04_s03

全绿;至少完成 1 个「浅拷贝崩溃复现 + 修复对比」。

返回目录