title: C/C++重难点
date: 2020-03-03 09:59:23
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++ 中的 struct
为了让C语言开发者们适应,C++故意保留了关键字struct
。但是,C++中的struct
再也不是昔日的struct
了!
2.1. struct in C
C语言中的struct
:
- 只能包含变量
- 不能包含函数,但是,可以包含函数指针(其实也是一种变量)
- 使用
malloc()
和free()
函数,在堆上动态分配、释放内存空间
示例:
#include
#include
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
- 可以有构造函数和析构函数
- 可以继承与被继承
- 使用
new
、delete
关键字创建、释放堆上的内存空间 - 可以包含虚函数
- 等等等等。。。
但要说区别的话,struct
与class
主要有以下几点区别:
- 默认的继承访问权限不同。struct默认是public继承(父类的public和protected,依旧是子类的public和protected),class默认是private继承(父类的public和protected,将成为子类的private)。
- 默认的访问控制权限不同。struct默认是public成员,class默认是private成员。
- class关键字能用于定义模板参数,但是struct不能。
示例:
#include
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);
}
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!
鄙人面试之时,就是因为这个问题而凉透的。
心痛示例:
#include
#include
#include
using namespace std;
typedef struct {
int count;
int time;
} Data;
typedef struct {
int sum_count;
queue 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):
ok
ok
没错,就是这么奇怪的输出。其中原因,我猜是使用malloc
创建Window
变量时,没有调用queue q
成员对象的构造函数,导致queue q
没有创建成功,使用过程时出现不可抗拒的BUG。
2.4. C++ 中什么时候使用 struct 什么时候使用 class
- 当解决简单问题,选择轻量级的 struct
- 当解决复杂问题,需使用抽象思维时,选择 class
3. STL 中的各种 push 函数,都是浅拷贝
浅拷贝是指:如果拷贝对象中存在指针类型成员变量,那么会将 旧对象的指针成员变量 直接赋值给 新对象的指针成员变量 ,导致新旧对象的指针成员变量都指向同一块内存区域。
示例:
#include
#include
#include
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 &v) {
char name[10] = "jerry"; // 在栈上创建
v.push_back(Person(name, 19));
}
int main() {
vector 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();
}
输出:
name:tome, age:18
name:dome, age:18
name:, age:19
如果,拷贝对象的指针成员变量指向堆上的内存空间,那么,只要不在新对象外部修改堆上的内存空间,就算小隐患;
如果,拷贝对象的指针成员变量指向栈上的内存空间,一旦栈空间被释放,那将是大大的隐患。
解决浅拷贝问题,则需要定义拷贝构造函数,在该函数中,进行深拷贝:
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 []
搭配使用:
char *name = new char[10];
...
delete [] name;
千万不可:
char name[10] = "error";
...
delete [] name;
5. 虚函数的内部实现
C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现
虚函数的本质是一个简单的虚函数表。
当一个类存在虚函数时,通过该类创建的对象实例,会在内存空间的前4字节保存一个指向虚函数表的指针__vfptr
。
__vfptr
指向的虚函数表,是类独有的,而且被该类的所有对象共享。虚函数表的实质,是一个虚函数地址的数组,它包含了类中每个虚函数的地址,既有当前类定义的虚函数,也有覆盖父类的虚函数,也有继承而来的虚函数。
当子类覆盖了父类的虚函数时,子类虚函数表将包含子类虚函数的地址,而不会有父类虚函数的地址。
同时,当用基类指针指向子类对象时,基类指针指向的内存空间中的__vfptr
依旧指向了子类的虚函数表。所以,基类指针依旧会调用子类的虚函数。
见如下示例:
5.1. 自己定义了虚函数的类
class Base1 {
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
定义两个对象:
Base1 b1;
Base1 b2;
两个对象的内存空间分配如下:
5.2. 既包含覆盖虚函数,又包含继承虚函数的类
class Base1 {
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Derive1 : public Base1 {
public:
int derive1_1;
int derive1_2;
// 覆盖基类函数
virtual void base1_fun1() {}
};
定义一个子类对象:
Derive1 d1;
其内存空间如下:
由图可以看出:
Base1 *b_p = &d1; // 指向子类对象的基类指针
b_p->base1_fun1(); // 调用子类虚函数
6. inline 和 #define 的区别
参考博客
#define
宏定义表达式的例子如下:
#define Expression(Var1,Var2) (Var1+Var2)*(Var1-Var2)
这种表达式形式宏形式与作用跟函数类似,但它使用预编译器,没有堆栈,使用上比函数高效。但它只是预编译器上符号表的简单替换,不能进行参数有效性检测及使用C++类的成员访问控制。
改为inline
定义:
inline int Expression(int Var1, int Var2) {
return (Var1+Var2)*(Var1-Var2);
}
inline
推出的目的,也正是为了取代这种表达式形式的宏定义。它保持了宏定义的优点,预编译时进行替换,高效。同时,它又是个真正的函数,调用时有严格的参数检测。它也可作为类的成员函数。
7. const 指针 和 指向 const 指针
const int *p; // 指向 const 的指针
int *const p; // const 指针