构造函数

自动解决数据成员的初始化问题
构造函数是类的一种特殊成员,本质上也是类的成员函数,函数名和类名相同,注意构造函数没有返回类型(因为没有变量来接收返回值),可以有参数也可以没有
不论声明还是定义,构造函数名前面都不能出现返回值类型,函数体中不能有return语句

当创建类的一个新对象时,构造函数被自动调用,完成对象的初始化工作,例:

class Clock
{
private:
    int hour,minute,second;
public:
    Clock(int h,int m,int s):hour(h),minute(m),second(s){};
};
int main(){
    Clock clock(1,2,3);
    return 0;
}

实现构造函数

声明构造函数后需要实现构造函数:初始化数据成员

有两种实现构造函数的方式
(在c++11的扩展以前,类的声明体内是不能初始化成员变量的,只有一个特例是static const int类型)

  • 赋值语句的方式:

Clock(int h,int m,int s){hour=h,minte=m,second=s;}

  • 参数初始化表的方式:

Clock(int h,int m,int s):hour(h),minute(m),second(s){}

需要注意的是在函数首部的小括号后面,参数初始化表前加上’:’,定义函数需要加上花括号

只对比两种构造方法本身的话,参数初始化表并没有效率上的优势,但是书写方便,尤其是成员变量较多时,这种写法非常简明明了,并且对于类的const成员,只能使用初始化列表,而不能在构造函数内部进行赋值操作。实际上更准确的来说,采用赋值语句的方式只能叫赋值而不能叫初始化,构造函数的初始化过程默认是在读取初始化表后完成的,因此如果不在初始化表中完成const成员的初始化,那么就相当于没有对const成员初始化,所以就产生错误了。
(当然也可以在类定义的时候,就对const成员变量进行赋值,但是这样操作的话这个变量有可能就失去了意义,因为基于这个类生成的所有对象const成员的值都为同一个常量)
另一方面,观察初始化表的形式可以发现对类对象成员的初始化可以直接调用其构造函数,反之如果要在构造函数中以赋值的形式对这些成员进行初始化的话,此时编译器已经调用默认构造函数(默认是无参数形式的,因为此时传入的参数默认是要在构造函数体内使用的)对该对象成员进行初始化了,无法对其再用拷贝构造函数进行初始化,因此往往只能重载=运算符进行赋值,造成其他不必要的开销。(实际上相当于无意义地多调用了一次默认构造函数)
综上,一个好的原则是能使用初始化列表的时候尽量使用初始化列表

使用构造函数进行对象初始化

向构造函数传参

传给构造函数实参有两种方式

  • 如果构造函数只有一个参数,可以赋值的形式初始化,基本格式为:

类名 对象名=实参,如:
class Counter{int value;Counter(int v):value(v){}};
Counter counter=10;

  • 如果构造函数有一个或多个参数,可以传入参数列表,

基本格式为:类名 对象名(参数列表),如:
Counter counter(10);
Clock clock1(1,2,3);

  • 如果构造函数有参数,但在创建对象时没有给出指定位置的参数,编译出错

重载构造函数

一个类可以提供多种不同形式的构造函数,即构造函数的重载
重载的目的是为了满足不同的初始化对象需要,如:

Clock(int h,int m,int s);
Clock clock(23,12,0);

Clock();
Clock clock; 最好不要写成clock(),有的编译环境下默认这是在声明返回类型为Clock的函数

Clock(char *timestr);
Clock clock(“23:12:00”);

具有默认参数的构造函数

构造函数也可以有默认参数,如:

Clock(int h=0,int m=0,int s=0);

值得注意的是如果在类外实现构造函数时则一般不说明缺省值,因为之前已经声明过了,而构造函数的作用域一般又相同,但是如果两次声明不一致的话,编译器就不知道调用哪个构造函数了
另外也要注意不能与重载函数的匹配格式相同,以免发生冲突

默认构造函数

对于没有构造函数的类,编译器会自动为其生成一个没有参数,不做任何工作的合成默认构造函数(空构造函数)
但是只要声明了一个任何形式的构造函数,系统自定义的构造函数就没有了,此时如果还需要一个空构造函数的话,必须再重载一个
因此对于一个对象而言,无论是否定义了构造函数,在生成对象时都一定会有一个构造函数被调用

注意:合成默认构造函数不会初始化类的内置类型及复合类型的数据成员
从本质上来说,对类中的内置变量和复合变量的初始化是编程者份内的事,并不应当作为编译器的任务,编译器要做的仅仅是合成一个构造函数能使对象创建起来并保证基本情况的运行,哪怕这个构造函数什么都没做,例:

class Test{
public:             //没有显式定义的构造函数
    int num;  
    bool istrue;
};
int main(){
    Test test;
    if(istrue)printf("%d",test.num); // 这里程序运行的行为是未定义的
    return 0;
}

只有在编译器需要默认构造函数来完成编译任务的时候,编译器才会为没有任何构造函数的类合成一个默认构造函数,或者是把这些操作插入到已有的构造函数中去。

  • 调用对象成员或基类的默认构造函数
  • 为对象初始化虚表指针与虚基类指针

合成的过程类似于在一个构造函数中嵌入另一个构造函数,如:

A::A(int va):a(va);
假设B是A的成员对象类或基类等情况需要调用构造函数
当需要合成默认构造函数时,编译器的行为类似于在A()中嵌套一个空的构造函数
A::A(int va):a(va){B::B();};

析构函数

有时在释放类时需要做一些收尾工作,此时可能因为忘记调用这些做收尾工作的函数而出现问题,对此c++提供了析构函数专门用于处理对象销毁时的清理工作,与构造函数相对应
析构函数没有返回类型,没有参数,函数名是在类名前加’~’,调用方法与其他成员函数相同,例:

class Clock
{
private:
    int hour,minute,second;
public:
    Clock(int h,int m,int s);
    ~Clock();
};
Clock::~Clock(){cout<<"对象已销毁"<<endl;}
int main(){
    Clock clock(1,1,1);
    clock.~Clock();
}

析构函数会在对象生存期结束后被自动调用
同构造函数,如果没有显示声明,编译器自动生成一个缺省的空析构函数
析构函数尤其在对象中成员的生命期不一致时具有较大的意义(这实际是因为默认析构函数的行为和正常的内存回收机制一样,不能回收堆内存,堆内存必须手动释放)

如:对象初始化时使用指针指向动态内存,在对象生存期结束之前就应该释放掉该内存空间,因此这类操作一般在析构函数中进行处理

此处再次强调!动态内存分配极容易出错,导致崩溃

  • 删除动态内存失败
  • 读写已删除的对象
  • 对同一内存空间使用多次delete(一个指针情况下多次删除)

拷贝构造函数

如果将与自己同类的对象的引用作为参数进行构造函数初始化,该构造函数称为拷贝构造函数,如:

Clock(Clock &obj):hour=obj.hour,minute=obj.minute,second=obj.second{}

调用拷贝构造函数进行对象初始化同样也有两种方法:直接调用和赋值形式

Clock clock1(1,2,3);
Clock clock2(clock1),clock3=clock1;

注意用等号赋值的形式并不是真正的用对象赋值,而是调用了拷贝赋值函数(实质上这里的赋值运算符是编译器的默认运算重载符函数)
拷贝构造函数的特点:

  • 首先,拷贝构造函数实质上就是一个构造函数,创建对象时系统会自动调用
  • 其次,拷贝构造函数将一个已经创建好的对象作为参数,根据需要将该对象中的数据成员逐一对应地赋值给新对象

如果没有定义拷贝构造函数,则编译器也会为该类定义一个缺省的拷贝构造函数,缺省的拷贝构造函数采用的是精确的位拷贝(拷贝内存)方法来完成对象到对象的复制,将第一个对象的数据成员值原封不动拷贝到第二个对象数据成员中,如:

Clock clock1(1,2,3),clock2=clock1;
std::cout<<memcmp(&clock1,&clock2,sizeof(Clock));输出结果为0

注意:这是在成员都没有自定义的拷贝构造函数时才这么做(其实这种情况和结构体很类似,因此都采用位拷贝模式)
如果一个类:

  • 本身包含、从基类继承、类的成员对象包含了虚函数
  • 成员变量有显式的拷贝构造函数

那么显然不能采用位拷贝的模式,此时是按成员拷贝的

除创建新对象时可能被调用外,拷贝构造函数还经常在以下情况被调用:

  • 对象作为函数参数
  • 函数返回对象

构造函数,析构函数,拷贝构造函数实例程序:自定义简易的Cstring类

#include<iostream>
#include<cstring>
using namespace std;
const int n=64;
class Cstring
{
private:
    int size;
    char *str;
public:
    Cstring(int len);
    Cstring(Cstring &cstring);
    void Copystring(char *string);
    void Showstring();
    ~Cstring();
};
Cstring::Cstring(int len):size(len)
{
    str=new char[size];
}
Cstring::Cstring(Cstring &cstring):size(cstring.size)
{
    str=new char[size];
    strcpy(str,cstring.str);
}
void Cstring::Copystring(char *string)
{
    strcpy(str,string);
    return;
}
void Cstring::Showstring()
{
    cout<<str;
}
Cstring::~Cstring()
{
    delete []str;                    //析构函数中释放从堆中分配的内存
}
void func1()
{
    Cstring cstring1(n),cstring2=cstring1;
    cstring1.Copystring("hello");
    cstring2.Copystring("world");
    cstring1.Showstring();
    cstring2.Showstring();
    cstring1.~Cstring();
    cstring2.~Cstring();             //试想如果此处不释放内存的话,func函数被调用结束后,cstring对象被释放,但其中动态分配的空间并没有被释放
}                                    //很可能发生崩溃
Cstring func2(Cstring cstring)
{
    cstring.Copystring("hello world");
    return cstring;
}
int main()
{
    Cstring cstring1(n),cstring2(n);
    func1();
    cstring2=func2(cstring1);       //第一次缺省的拷贝构造函数调用:形参cstring初始化为实参cstring1的值
    cstring2.Showstring();          //第二次缺省的拷贝构造函数调用:func2()返回局部变量cstring时使用临时变量存储cstring的值
}                                   //第三次缺省的拷贝构造函数调用:使用cstring2接受func2(cstring1)的值