C/C++重难点


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
  • 可以有构造函数和析构函数
  • 可以继承与被继承
  • 使用newdelete关键字创建、释放堆上的内存空间
  • 可以包含虚函数
  • 等等等等。。。

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

  1. 默认的继承访问权限不同。struct默认是public继承(父类的public和protected,依旧是子类的public和protected),class默认是private继承(父类的public和protected,将成为子类的private)。
  2. 默认的访问控制权限不同。struct默认是public成员,class默认是private成员
  3. 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;

两个对象的内存空间分配如下:

1.png

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;

其内存空间如下:

2.png

由图可以看出:

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 指针

你可能感兴趣的:(C/C++重难点)