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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>

int My_Add(int a, int b) {
return a + b;
}

int My_Sub(int a, int b) {
return a - b;
}

typedef struct {
int data;
int(*Add) (int, int); // 函数指针
int(*Sub) (int, int);
} CTest;

int main() {
CTest test;
test.data = 0;
test.Add = My_Add;
test.Sub = My_Sub;
printf("%d\n", test.Add(1, 2));

CTest *test_p = (CTest*)malloc(sizeof(CTest));
test_p->data = 1;
test_p->Add = My_Add;
test_p->Sub = My_Sub;
printf("%d\n", test_p->Add(1, 2));
return 0;
}

2.2. struct in C++

而,C++中的struct,其实就是class的简约版,包含大部分class的特性:

  • 既能包含变量,又能包含函数
  • 支持运算符重载
  • 可以使用访问修饰符:privat, protected, public
  • 可以有构造函数和析构函数
  • 可以继承与被继承
  • 使用newdelete关键字创建、释放堆上的内存空间
  • 可以包含虚函数
  • 等等等等。。。

但要说区别的话,structclass主要有以下几点区别:

  1. 默认的继承访问权限不同。struct默认是public继承(父类的public和protected,依旧是子类的public和protected),class默认是private继承(父类的public和protected,将成为子类的private)。
  2. 默认的访问控制权限不同。struct默认是public成员,class默认是private成员
  3. class关键字能用于定义模板参数,但是struct不能。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <cstdio>

struct CPP_Struct1 {
int data;
};

// 与 class CPP_Struct2 简直一样
struct CPP_Strcut2 {
int data;

// 构造函数
CPP_Strcut2(int a):data(a) {}

// 方法
void print() { printf("data:%d\n", this->data); }

// 重载运算符,const {} 表示函数不会修改自身属性
bool operator < (const CPP_Strcut2 &a) const {}

// 虚函数
virtual void virtual_test() { printf("virtual in struct"); }
};

int main() {
// 无构造函数
CPP_Struct1 test1; // 等价于:CPP_Struct1 test1 = CPP_Struct1 test1()。调用默认构造函数,分配在栈上
test1.data = 1;
printf("%d\n", test1.data);

CPP_Struct1 test1_1{1}; // 无构造函数时,可以这样操作
printf("%d\n", test1_1.data);

CPP_Struct1 *test1_p = new CPP_Struct1; // 调用默认构造函数,分配在堆上
test1_p->data = 2;
printf("%d\n", test1_p->data);
delete test1_p;

// 有构造函数
CPP_Strcut2 test2(1); // 隐式地调用构造函数,等价于:CPP_Strcut2 test2 = CPP_Strcut2(1)。分配在栈上
test2.print();
test2.virtual_test();

CPP_Strcut2 *test2_p = new CPP_Strcut2(2); // 分配在堆上
test2_p->print();
test2_p->virtual_test();
delete test2_p;

return 0;
}

2.3. 一定要在 C++ 中使用 new 创建 struct

有C语言使用习惯的人,总喜欢用malloc()函数来为struct分配堆空间。

但要特别注意的是:在C++中,一定要使用new来为struct分配堆空间,而不要使用malloc()函数!!!因为,malloc()函数不会调用构造函数!!!

有小伙伴可能会说,那在struct中不定义构造函数不就行了?

但问题是,C++中的struct就相当于class类。而你有可能在struct定义了其它类的成员对象。如果,你在创建struct时,不调用构造函数。那么,成员对象的构造函数将不会被调用,成员对象的创建将出现不可抗拒的BUG!

鄙人面试之时,就是因为这个问题而凉透的。

心痛示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdio>
#include <cstdlib>
#include <queue>

using namespace std;

typedef struct {
int count;
int time;
} Data;

typedef struct {
int sum_count;
queue<Data> q;
} Window;

int main() {
printf("ok\n");
Window *p = (Window*)malloc(sizeof(Window));
printf("ok\n");
p->q.push(Data{1, 0});
printf("ok\n");
printf("count:%d, time:%d\n", p->q.back().count, p->q.back().time);
return 0;
}

最后输出(MinGW-W64 g++ 8.1.0):

1
2
ok
ok

没错,就是这么奇怪的输出。其中原因,我猜是使用malloc创建Window变量时,没有调用queue<Data> q成员对象的构造函数,导致queue<Data> q没有创建成功,使用过程时出现不可抗拒的BUG。

2.4. C++ 中什么时候使用 struct 什么时候使用 class

  • 当解决简单问题,选择轻量级的 struct
  • 当解决复杂问题,需使用抽象思维时,选择 class

3. 浅拷贝

浅拷贝是指:如果拷贝对象中存在指针类型成员变量,那么会将 旧对象的指针成员变量 直接赋值给 新对象的指针成员变量 ,导致新旧对象的指针成员变量都指向同一块内存区域。

STL 中的各种 push 函数,都是浅拷贝。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <cstdio>
#include <vector>
#include <string>

using namespace std;

struct Person {
char *name;
int age;
Person(char *name, int age): age(age) {
this->name = new char[strlen(name) + 1]; // 在堆上创建
strcpy(this->name, name);
}
void print() {
printf("name:%s, age:%d\n", name, age);
}
~Person() {
delete[] name; // new [] 一定要用 delete [] 释放
}
};

void add(vector<Person> &v) {
char name[10] = "jerry"; // 在栈上创建
v.push_back(Person(name, 19));
}

int main() {
vector<Person> v;

char *name = new char[10]; // 在堆上创建
sscanf("tome", "%s", name);
v.push_back(Person(name, 18));
v[0].print();
name[0] = 'd';
v[0].print();

v.clear();
add(v);
v[0].print();
}

输出:

1
2
3
name:tome, age:18
name:dome, age:18
name:, age:19

如果,拷贝对象的指针成员变量指向堆上的内存空间,那么,只要不在新对象外部修改堆上的内存空间,就算小隐患;

如果,拷贝对象的指针成员变量指向栈上的内存空间,一旦栈空间被释放,那将是大大的隐患。

解决浅拷贝问题,则需要定义拷贝构造函数,在该函数中,进行深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Person {
char *name;
int age;

// 构造函数
Person(char *name, int age): age(age) {
this->name = new char[strlen(name) + 1]; // 在堆上创建
strcpy(this->name, name);
}

// 拷贝构造函数,用于解决浅拷贝的问题
Person(const Person &p) {
this->name = new char[strlen(p.name) + 1]; // 深拷贝
strcpy(this->name, p.name);
this->age = p.age;
}

void print() {
printf("name:%s, age:%d\n", name, age);
}

// 析构函数
~Person() {
delete[] name; // new [] 一定要用 delete [] 释放
}
};

4. delete [] 只能释放 new [] 申请的空间

delete []必须与new []搭配使用:

1
2
3
char *name = new char[10];
...
delete [] name;

千万不可:

1
2
3
char name[10] = "error";
...
delete [] name;

5. 虚函数

参考我的另一篇博客:理解C++虚函数

6. inline 和 #define 的区别

参考博客

#define宏定义表达式的例子如下:

1
#define Expression(Var1,Var2) (Var1+Var2)*(Var1-Var2)

这种表达式形式宏形式与作用跟函数类似,但它使用预编译器,没有堆栈,使用上比函数高效。但它只是预编译器上符号表的简单替换,不能进行参数有效性检测及使用C++类的成员访问控制(public等)。

改为inline定义:

1
2
3
inline int Expression(int Var1, int Var2) {
return (Var1+Var2)*(Var1-Var2);
}

inline推出的目的,也正是为了取代这种表达式形式的宏定义。它保持了宏定义的优点,预编译时进行替换,高效。同时,它又是个真正的函数,调用时有严格的参数检测。它也可作为类的成员函数。

7. const 指针 和 指向 const 指针

1
2
const int *p;   // 指向 const 的指针
int *const p; // const 指针

8. 拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它的参数必须是本类型的一个引用变量

1
2
3
class Example {
Example(const Example &e) {}
}

如果参数不是引用变量,则调用时会陷入死循环(实参赋值到形参时,也会调用拷贝构造函数)。

9. static_cast 和 dynamic_cast

C 语言的类型转换:

1
2
3
// (type)expr
int i;
double b = (double)i;

现代 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
2
3
// static_cast<type>(expr)
int i;
double b = static_cast<double>(i);
  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类中可能包含父类中没有的字段或方法)

dynamic_cast

  • 用于多态类型(基类有虚函数)的转换
  • 运行时进行类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换(子类转为基类)、向下转换(基类转换为子类,具有类型检查的功能,比static_cast更安全)