本文需要C++语言基础后食用

最近在复习C++,在复习OOP时发现C++中的许多内容与C#并不相同,甚至可以说相差甚远。特以此文来详细描述一下。

OOP基础

为了照顾没有OOP基础的同学,先来简单说一下类和对象。

类和对象

class Human
{
    //attributes
    string name;
    string dataOfBirth;
...
    //function
    void talk(){}
    void walk(){}
...
}

OOP即面向对象编程,而类是一种可以模拟现实事物的结构,如上面的代码片段模拟的人一样。其中的所有属性(如string name等)及方法(属于类成员的函数)都称为类的成员。 类相当于蓝图,而要在程序执行时使用类的功能需要使用类的实例——对象。相关的语法在这里就不一一赘述了。

构造函数、析构函数

从这里开始C++和C#就有区别了,后者中没有复制构造函数的概念,这主要是因为两者的引用语义不同,而这里主要讨论C++中的情况,两者比较不作讨论。

构造函数

当类被实例化为对象时,便会调用那个类的构造函数,且只会在被实例化的时候调用一次。构造函数可以被重载,它与类同名且不返回值。构造函数如同它的名字,非常适合作为初始化成员变量的场所。

class Human{
    string name;
    int age;
    ...
};
Human::Human()//声明构造函数
{
    age=1//初始化成员变量
    ...
}

在构造函数中初始化可以确保变量中不包含垃圾值。

析构函数

在对象被销毁或不再在作用域内时,都会调用析构函数。因此在析构函数中重置变量或释放分配的内存比较合适。在C中,你必须自己分配并管理内存,如char*;而C++中使用std::string等工具(实际是类)充分利用了析构函数和构造函数,相信你已经体验到了它的便利。

函数调用在处理器层面的实现

为了从根本上说明复制析构函数,需要了解处理器层面上的函数调用。

函数调用意味着微处理器跳转到属于被调用函数的下一条指令处执行。执行完函数的指令后,将返回到最初离开的地方。

栈是一种先进后出的数据结构。就如同一叠盘子,最后放上去的那个盘子会被第一个拿出来。把数据存入栈中称为压入,取出数据称为弹出。函数的调用过程通俗来说,就是当程序执行到调用函数1,处理器将调用完函数后要执行的指令位置保存到栈中,然后跳转到含有调用函数的内存地址。执行完调用函数的指令后2,再弹出刚才压入的地址,继续执行接下来的指令。栈帧于此不过多介绍。

以上内容对于理解内联函数有很大帮助

复制构造函数

浅复制及其问题

当对象中包含有指针成员,而复制该对象后其中新的指针成员与原对象的指针成员指向同一内存地址。销毁其中任何一个对象将会导致另一个对象中的指针变为野指针,导致程序崩溃。这种不复制指向的内存单元的复制称为浅复制。调用函数时,实参会被复制给形参,而对象同样可以作参数。当对象按值传递,编译器执行二进制复制,二进制复制不会复制指向的内存单元。

使用复制构造函数确保深复制

复制构造函数是一个重载的构造函数,接受一个按引用传递的对象作为参数。

class MyString
{
    MyString(const MyString& copySource);
};

在对象被复制的时候都会调用复制构造函数,而调用复制构造函数的情况有三种。

  • 定义一个新对象并用同类型的对象对它进行初始化
  • 该类型的对象作为参数传递给函数时
  • 从函数返回该类型的对象时

总而言之,已有对象复制一个新对象时就会调用复制构造函数。

为什么必须是按引用传递的对象?

拜托,阿sir,看看第二条,要是对象作为参数传递给复制构造函数,那它就会无限调用自身直到堆栈溢出崩溃啊。

为什么要加const修饰词?

一般约定加const声明,使参数值不能改变,以免调用函数时不慎使对象值被修改。没有使用原始指针的情况下,一般不需要编写复制构造函数。

未完待续~


  1. 编译器会将函数调用转化为一条CALL指令。 
  2. 到达与return语句对应的RET指令。 

盛夏日落迟 灯火未夜匆匆明