C++ / MFC / Qt / C# 核心知识点汇总笔记
目录
- 浅拷贝 & 深拷贝 & 内存分布
- new 底层做了什么
- 类有指针成员为什么必须重载赋值运算符
- Qt 信号槽 & &符号含义 & 函数指针原理
- MFC _T() / TCHAR / w宽字符 详解
- MFC 为什么大量用 #define 宏
- MFC 为什么不用虚函数、偏用宏做消息映射
- WinForm 与 MFC 设计差异
- 模态对话框 含义 & 由来
- 常量指针 & 指针常量 区分
1. 浅拷贝 & 深拷贝 & 内存分布
浅拷贝
编译器默认拷贝构造/默认赋值,只按字节逐成员赋值 。
类中有裸指针时:只拷贝指针地址,不新开堆内存。
- 两个对象指针指向同一块堆内存
- 修改一个对象的指针内容,另一个跟着变
- 析构时重复释放同一块内存 → 程序崩溃
深拷贝
手动自定义拷贝构造、重载赋值运算符:
- 不直接赋值指针地址
- 重新
new/malloc开辟独立堆内存 - 再把内容拷贝过去
内存分布对比
浅拷贝内存
栈区:p1、p2 对象各自独立
p1.pName 、p2.pName 存同一个堆地址
堆区:只有一份字符串内容,两人共用
深拷贝内存
栈区:p1、p2 对象独立
堆区:p1 一份内存、p2 单独新开一份内存
各自指向独立堆空间,互不干扰
修改 p1.pName 影响
- 浅拷贝:改 p1 会连带改 p2
- 深拷贝:改 p1 完全不影响 p2
2. new 底层做了什么
Person* p = new Person("xxx",20); 底层三件事:
- 在堆内存申请一块足够存放对象的空间
- 调用类构造函数初始化成员
- 把堆起始地址返回给栈上的指针
p
对象内存位置
- 指针变量
p:在栈 - Person 对象本体:在堆
- 对象内指针成员再指向另一块堆字符串
delete 底层做什么
- 先调用析构函数(释放内部指针堆内存)
- 释放 new 申请的对象本身堆内存
关键
new 出来的堆对象不会自动析构,必须手动 delete。
3. 类有指针成员 必须重载=运算符?
结论
类包含裸指针 、构造里手动 new/malloc、析构要 delete/free
👉 必须自定义:拷贝构造 + 赋值运算符重载 + 析构(C++三法则)
不重载=的后果
- 默认赋值是浅拷贝,指针同指向一块堆内存
- 旧内存丢失 → 内存泄漏
- 析构重复释放 → 程序崩溃
什么时候不用重载
成员都是 int/double/std::string 等,无裸指针、无手动堆内存申请。
用 std::string 替代 char* 可彻底避开浅拷贝问题。
4. Qt 信号槽 & &符号 & 函数指针底层
Qt信号槽 对应 C#
- Qt
signals👉 C#event - Qt
slots👉 C# 普通订阅方法 - Qt
connect()👉 C#+=绑定 - Qt
emit👉 C#Invoke()触发
& 在 &类::函数名 含义
- 普通函数名本身就是地址,可隐式转函数指针
- 类成员函数 不能隐式转地址,C++语法强制加
&
👉&Sender::mySignal= 显式取成员函数入口地址,当做函数指针传参
为什么成员函数必须加&
- 成员函数隐含
this指针,不是独立裸函数 - 类有函数重载,
A::f有语法歧义,分不清重载版本 - 成员函数指针不是单纯地址,是复合结构
- C++标准禁止成员函数名隐式转地址
信号槽底层原理
本质:哈希映射表 + 成员函数指针遍历调用
connect:把 信号地址、接收对象、槽函数指针 存入映射表emit:查表 → 遍历所有绑定的槽 → 逐个通过函数指针调用
跨线程信号槽:不是直接调用,而是封装成消息入队列,在目标线程执行。
函数指针怎么知道函数起止内存
- 函数指针只存起始地址
- 不需要存结束地址,CPU 执行到汇编
ret指令自动结束函数 - 若要人为获取起止,需解析PE文件/编译器扩展。
5. MFC _T() / TCHAR / 宽字符
_T() 作用
适配 ANSI(多字节) / Unicode(宽字符) 双字符集,一套代码通用。
底层宏定义
c
#ifdef _UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif
- Unicode:
_T("abc")→L"abc" - 多字节:
_T("abc")→"abc"
w 前缀含义
w = wide 宽字符
char:1字节窄字符wchar_t:2字节宽字符,可存中文L"字符串":宽字符串字面量wcslen/wcscpy:宽字符字符串函数
配套通用类型
TCHAR:自动适配 char / wchar_tLPCTSTR/LPTSTR:自适应字符串指针_tcsxxx:自适应字符串函数
三种字符串区别
"xxx":固定 char 窄字符串L"xxx":固定 wchar_t 宽字符串_T("xxx"):编译时自动适配
6. MFC 为什么大量用 #define 宏
- 适配ANSI/Unicode双字符集(最主要)
- 简化极复杂C++语法(消息映射宏)
- 兼容不同编译器、VC版本、32/64位架构
- 功能开关裁剪,控制编译模块
- 生成重复模板代码,减少手写量
7. MFC 为什么不用虚函数,偏用宏做消息映射
- 年代硬件差:全做虚函数,虚表内存开销爆炸,99%消息用不到却要全承载
- Windows 消息无限新增,虚函数基类要频繁改动、全部重编译
- 虚函数编译期绑定,不支持运行时消息路由、动态拦截
- 要兼容原生C语言窗口回调机制,虚函数适配困难
- 宏可自动生成映射表模板代码,统一规范
8. WinForm 对比 MFC
- WinForm 高层封装,彻底屏蔽原生Win32消息,无宏、无消息映射
- C# 原生事件+委托 直接替代 MFC 消息映射
- 硬件性能充足,不在乎虚表内存开销,直接用虚函数重写
- WinForm 底层封装Win32,上层只暴露
Click/Load/Paint等高阶事件 - WinForm:
Show()非模态 /ShowDialog()模态
9. 模态对话框 含义 & 由来
通俗理解
模态 :强制锁定在当前弹窗模式,必须处理完关闭后,才能操作父窗口
非模态:弹窗打开,仍可自由操作主窗口。
底层原理
- 模态:禁用父窗口,内部独立消息循环,阻塞等待关闭
- 非模态:不禁用父窗口,共用消息循环
调用方式
- MFC:模态
DoModal();非模态Create()+ShowWindow() - WinForm:模态
ShowDialog();非模态Show()
模态词源
源自拉丁语 modus 模式/状态;
逻辑学、语法情态动词引申为强制、必须遵从当前模式 ;
GUI借用该概念:强制用户停留在弹窗交互模式。
10. 常量指针 & 指针常量
记忆规则
const 离谁近,谁不可修改;
const 在*左:常量指针;const在*右:指针常量。
常量指针
cpp
const int *p;
- 指向的值 不可改
*p = 100报错 - 指针本身指向 可以改
p = &b
口诀:指向常量,指针能变
指针常量
cpp
int *const p;
- 指针本身指向 不可改
- 指向的值 可以改
*p = 100
口诀:指针本身是常量,指向不能变
双重锁定
cpp
const int *const p;
值不能改、地址也不能改。
C++ 指针、数组、数组指针、指针数组、[]与.-> 全套精讲
一、基础代码示例
cpp
class Person
{
public:
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
void showPerson() {
cout << name << " " << age << endl;
}
};
// 动态开辟对象数组
Person* arr2 = new Person[2]{ Person("lucy",18), Person("bob",20) };
// 四种等价调用
(*(arr2 + 0)).showPerson();
arr2[0].showPerson();
(arr2 + 1)->showPerson();
(*(arr2 + 1)).showPerson();
delete[] arr2;
二、为什么 Person* arr2 可以用 arr2[0]
1. 核心等价公式(C++ 铁律)
cpp
p[i] <==> *(p + i)
只要是指针,指向连续同类型内存,都可以用 [下标] 写法。
2. 逐层推导
arr2类型:Person*(指向数组首元素的指针)arr2 + 0:还是Person*指针*(arr2 + 0):解引用 ,拿到实体对象 Personarr2[0]等价于*(arr2+0)→ 本身就是对象
3. 关键区分
arr2 + i→ 指针Person*arr2[i]→ 实体对象Person
三、为什么 arr2[0] 是对象,不是指针
arr2是Person*,存的是首元素地址arr2[0]自带隐式解引用*- 解引用后,访问到内存里真实的 Person 实例
- 类型从
Person*变成Person
通俗比喻:
arr2 是手指(指针)
arr2+0 还是手指
arr2[0] 顺着手指找到真人(实体对象)
四、. 和 -> 运算符终极规则
1. 使用铁律
- 实体对象 / 引用 → 用
. - 对象指针 → 用
->
2. 等价对照
cpp
// 指针 -> 用 ->
(arr2 + 0)->showPerson();
// 解引用得到对象 -> 用 .
(*arr2).showPerson();
arr2[0].showPerson();
(*(arr2 + 0)).showPerson();
3. 错误写法
cpp
(*arr2)->showPerson(); // 错误:*arr2 是对象,不能用 ->
arr2[0]->showPerson(); // 错误:arr2[0] 是对象,不能用 ->
五、指针数组 vs 数组指针(彻底分清)
前置优先级
[] 优先级 高于 *
1. 指针数组
写法:Person* arr[2]
- 无括号,
arr[2]先结合 → 是数组 - 每个元素类型
Person*→ 数组里存指针 - 本质:数组装指针
内存:多个指针,可指向零散不连续对象。
2. 数组指针
写法:Person (*p)[2]
- 括号把
*p包起来 → 先结合成指针 - 后面
[2]→ 指向一整个含2个Person的数组 - 本质:指针指向整个数组
内存:一整块连续数组,p++ 直接跳过整个数组大小。
3. 普通指针(你代码里的)
写法:Person* arr2
- 既不是指针数组,也不是数组指针
- 只是指向数组首元素的普通对象指针
arr2++只跳过一个 Person 对象大小
4. 三者一眼区分
| 写法 | 名称 | 本质 |
|---|---|---|
Person* arr2 |
普通元素指针 | 指向单个/数组首元素 |
Person* arr[2] |
指针数组 | 数组,元素是指针 |
Person (*p)[2] |
数组指针 | 指针,指向整个数组 |
5. 记忆口诀
- 括号包星:数组指针
- 无括号先数组:指针数组
六、关键类型汇总
| 表达式 | 类型 | 用法 |
|---|---|---|
| arr2 | Person* | 指针 |
| arr2+0 | Person* | 指针,用 -> |
| *arr2 | Person | 对象,用 . |
| arr2[0] | Person | 对象,用 . |
七、一句话终极总结
- 指针可以用
[],arr2[i] = *(arr2+i) - 带下标
[]就是隐式解引用 ,得到实体对象,用. - 不带下标、
变量+i是指针,用-> - 无括号是指针数组,带括号包星是数组指针
Person* arr2只是普通首元素指针,不属于上面两者