C/C++重难点
0. 前言
实习生面试写代码的时候,由于C、C++混用,出现了不可抗拒的BUG,最终调试失败,面试凉凉。
事后,深感自己C++学得不扎实,又患有C与C++混用的毛病。因此,特开新篇,归纳自己在使用C++过程中,所遇到的重难点。
1. 简单区分 C 与 C++
记得上C语言课程时,我们都使用devcpp编辑器。但是,devcpp默认的编译器为g++
,默认的保存文件格式为.cpp
。这导致,我们在学习C语言过程中,就算使用了C++
的语法,编译器也不会报错,这是十分糟糕的!
区分 C 与 C++ ,我认为有两个关键点:
- C 的编译器为
gcc
,C++ 的编译器为g++
- C 文件名格式为
.c
,C++ 文件名格式为.cc
或.cpp
2. 对比 C 与 C++ 中的 struct
为了让C语言开发者们适应,C++故意保留了关键字struct
。但是,C++中的struct
再也不是昔日的struct
了!
2.1. struct in C
C语言中的struct
:
- 只能包含变量
- 不能包含函数,但是,可以包含函数指针(其实也是一种变量)
- 使用
malloc()
和free()
函数,在堆上动态分配、释放内存空间
示例:
1 |
|
2.2. struct in C++
而,C++中的struct
,其实就是class
的简约版,包含大部分class
的特性:
- 既能包含变量,又能包含函数
- 支持运算符重载
- 可以使用访问修饰符:
privat
,protected
,public
- 可以有构造函数和析构函数
- 可以继承与被继承
- 使用
new
、delete
关键字创建、释放堆上的内存空间 - 可以包含虚函数
- 等等等等。。。
但要说区别的话,struct
与class
主要有以下几点区别:
- 默认的继承访问权限不同。struct默认是public继承(父类的public和protected,依旧是子类的public和protected),class默认是private继承(父类的public和protected,将成为子类的private)。
- 默认的访问控制权限不同。struct默认是public成员,class默认是private成员。
- class关键字能用于定义模板参数,但是struct不能。
示例:
1 |
|
2.3. 一定要在 C++ 中使用 new 创建 struct
有C语言使用习惯的人,总喜欢用malloc()
函数来为struct
分配堆空间。
但要特别注意的是:在C++中,一定要使用new
来为struct
分配堆空间,而不要使用malloc()
函数!!!因为,malloc()
函数不会调用构造函数!!!
有小伙伴可能会说,那在struct
中不定义构造函数不就行了?
但问题是,C++中的struct
就相当于class
类。而你有可能在struct
中定义了其它类的成员对象。如果,你在创建struct
时,不调用构造函数。那么,成员对象的构造函数将不会被调用,成员对象的创建将出现不可抗拒的BUG!
鄙人面试之时,就是因为这个问题而凉透的。
心痛示例:
1 |
|
最后输出(MinGW-W64 g++ 8.1.0):
1 | ok |
没错,就是这么奇怪的输出。其中原因,我猜是使用malloc
创建Window
变量时,没有调用queue<Data> q
成员对象的构造函数,导致queue<Data> q
没有创建成功,使用过程时出现不可抗拒的BUG。
2.4. C++ 中什么时候使用 struct 什么时候使用 class
- 当解决简单问题,选择轻量级的 struct
- 当解决复杂问题,需使用抽象思维时,选择 class
3. 浅拷贝
浅拷贝是指:如果拷贝对象中存在指针类型成员变量,那么会将 旧对象的指针成员变量 直接赋值给 新对象的指针成员变量 ,导致新旧对象的指针成员变量都指向同一块内存区域。
STL 中的各种 push 函数,都是浅拷贝。
示例:
1 |
|
输出:
1 | name:tome, age:18 |
如果,拷贝对象的指针成员变量指向堆上的内存空间,那么,只要不在新对象外部修改堆上的内存空间,就算小隐患;
如果,拷贝对象的指针成员变量指向栈上的内存空间,一旦栈空间被释放,那将是大大的隐患。
解决浅拷贝问题,则需要定义拷贝构造函数,在该函数中,进行深拷贝:
1 | struct Person { |
4. delete [] 只能释放 new [] 申请的空间
delete []
必须与new []
搭配使用:
1 | char *name = new char[10]; |
千万不可:
1 | char name[10] = "error"; |
5. 虚函数
参考我的另一篇博客:理解C++虚函数
6. inline 和 #define 的区别
#define
宏定义表达式的例子如下:
1 |
这种表达式形式宏形式与作用跟函数类似,但它使用预编译器,没有堆栈,使用上比函数高效。但它只是预编译器上符号表的简单替换,不能进行参数有效性检测及使用C++类的成员访问控制(public等)。
改为inline
定义:
1 | inline int Expression(int Var1, int Var2) { |
inline
推出的目的,也正是为了取代这种表达式形式的宏定义。它保持了宏定义的优点,预编译时进行替换,高效。同时,它又是个真正的函数,调用时有严格的参数检测。它也可作为类的成员函数。
7. const 指针 和 指向 const 指针
1 | const int *p; // 指向 const 的指针 |
8. 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它的参数必须是本类型的一个引用变量。
1 | class Example { |
如果参数不是引用变量,则调用时会陷入死循环(实参赋值到形参时,也会调用拷贝构造函数)。
9. static_cast 和 dynamic_cast
C 语言的类型转换:
1 | // (type)expr |
现代 C++ 语言的类型转换:
1 | // cast-name<type>(expr) |
cast-name
主要有如下几种:
static_cast
Used for conversion of nonpolymorphic(非多态) types.dynamic_cast
Used for conversion of polymorphic types.const_cast
Used to remove the const, volatile, and __unaligned attributes.reinterpret_cast
Used for simple reinterpretation of bits.
其中,static_cast
格式如下:
1 | // static_cast<type>(expr) |
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int)
- 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类中可能包含父类中没有的字段或方法)
dynamic_cast
:
- 用于多态类型(基类有虚函数)的转换
- 运行时进行类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换(子类转为基类)、向下转换(基类转换为子类,具有类型检查的功能,比static_cast更安全)