类和对象-对象的初始化和清理

构造函数和析构函数

在用类实例化出来一个对象的时候,会默认调用构造函数;在使用完一个对象的时候,会默认调用析构函数。所谓构造函数,顾名思义就是在实例化一个对象的时候,为对象做一些比如赋初值这样的初始化操作;析构函数,就是在对象执行完毕后对象进行销毁,执行一些清理工作。

  • 构造函数:
  1. 没有返回值,连void也不写
  2. 函数名和类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象的时候会自动调用构造函数,无须手动动调用而且只会调用一次;
    语法:类名(){}
  • 析构函数
  1. 没有返回值,也不写void
  2. 函数名称与类名相同,但要在前面加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前调用析构函数,无须手动调用而且只会调用一次
    语法: ~类名(){}

来看一个例子:

class Person
{
public:
    //构造函数
    Person()
    {
        cout << "Person 构造函数调用" << endl;
    }
    ~Person()
    {
         cout << "Person 析构函数调用" << endl;
    }
};

int main(void)
{
    Person p; //在创建对象的时候会调用构造函数 销毁对象的时候调用析构函数
    return 0;
}

注意事项:

  • 构造函数和析构要在类外进行访问,所以要写在public作用域上才可以;
  • 如果我们没有手写构造和析构函数,编译器会为我们创建两个空的析构和构造函数。

构造函数的分类与调用

构造函数按参数分类有两种,有参构造和无参构造两种。本质上是函数的重载。

有参构造与无参构造

class Person
{
public:
    //构造函数
    Person() //无参构造(默认构造)
    {
        cout << "Person 无参构造函数调用" << endl;
    }
    Person(int a) //有参构造
    {
        cout << "Person 有参构造函数调用" << endl;
    }
};

从上面的例子中很容易看出,有参构造的函数参数列表有参数,无参构造的函数参数列表没有参数。

拷贝构造

所谓拷贝,就是复制的意思,顾名思义就是要复制一个类的属性,创建一个类。参数是另一个类。我们再来创建一个Person类,这次给一个属性Age,来看看如何使用拷贝构造:

class Person
{
public:
    //拷贝构造函数
    Person(const Person &p)
    {
        m_Age = p.m_Age; //将传入的类的所有属性,拷贝到自己身上
    }
    int m_Age;
};

留意一下拷贝构造传入的参数,是一个类的引用,并用const修饰。

调用

了解了拷贝构造的分类以及使用方法,我们来看一段完整的代码,看看如何对它们进行调用

class Person
{
public:
    //构造函数
    Person(int age)
    {
        cout << "Person有参构造函数调用:" << endl;
        m_Age = age;
    }
    Person()
    {
        cout << "Person无参构造函数调用:" << endl;
    }
    Person(const Person &p)
    {
        m_Age = p.m_Age;
        cout << "Person拷贝构造函数调用:" << endl;
    }
//析构函数
    ~Person() 
    {
          cout << "Person析构函数调用" << endl;
    }
    int m_Age;
};

int main(void)
{
    //1、括号法
    Person p1;   //调用默认构造函数
    Person p2(10); //调用有参构造函数
    Person p3(p2); //拷贝构造函数调用   将p2的属性拷贝给p3;

    cout << "p2的年龄为:" << p2.m_Age << "岁。" << endl; //10岁
    cout << "p3的年龄为:" << p3.m_Age << "岁。" << endl; //10岁

    //2、显示法
    Person p4 = Person(10); //等号右边如果单成一行代码,就是一个匿名对象
    Person p5 = Person(p2);

    //3、隐式转换法
    Person p6 = 10; //相当于 Person p6 = Person(10);   有参构造
    Person p7 = p2; //相当于 Person p7 = Person(p2);   拷贝构造
}

构造函数的初始化调用一共有三种,括号法,显示法,隐式转换法。使用最多的就是括号法了。

注意事项:

  • 默认构造的时候不要加()
    也就是说Person p1; 会默认调用默认构造,而不能写成Person p1();因为后者会被误认为是函数的声明;
  • 不要利用拷贝构造函数初始化匿名对象
    上述代码中,显示法调用的构造函数例子里面,等号右边就是一个匿名对象。如果等号右边单独写一行,在此行代码执行完毕后,就会销毁掉所实例化的对象。如果利用了拷贝构造初始化匿名对象,编译器会将Person(p3)当成Person p3。

拷贝构造函数的调用时机

  • 用已经创建的对象来初始化一个新的对象,这在之前的例子中有所体现。
void test01()
{
    Person p1(10); //有参构造
    Person p2(p1); //用p1来创建一个与之一摸一样的对象p2
}
  • 值传递的方式给函数传值
    由于值传递的本质是拷贝出来一个临时的副本,所以传递值的时候会调用拷贝构造函数
void work1( Person p)
{
     
}
void test02()
{
   Person p;  //有参构造
   work1(p); //拷贝构造
}
  • 值方式返回局部对象
    值得方式返回对象的时候类似于值传递对象,也会创建临时的副本返回(老师上课讲的意思)。
Person work2()  //值得方式返回第一个局部对象
{
    Person p1;
    cout << (int *)&p1 << endl;
    return p1;
}
void test03()
{
   Person p1= work2();
   cout << (int *)&p1 << endl;
}  

这里是有些问题的,因为我的编译环境运行出来的结果和老师的不一样。我运行上段代码发现根本没有调用拷贝构造函数,并且两者的输出的地址结果完全一样,并且是在第二个地址输出后才调用的析构函数。这说明返回一个对象的时候,并没有发生赋值,而是直接把原来的对象返回了,类似于返回引用值。于是我抱着试一试的态度再次写了一段代码来测试:

int func()
{
   int a = 10;
   cout << (int *)&a << endl;//0x61fddc
   return a;
}
void test04()
{
   int a = func();
   cout << (int *)&a << endl;//0x61fe1c
}  

这次的两次的地址不一样。但我觉得这不足以说明返回的时候会拷贝一个值回来,有可能是在接收的时候才发生了拷贝。总结以上两个例子,在返回对象和返回一个局部变量的情形是不一样的。

构造函数的调用规则

一般情况下,编译器至少会给以个类添加三个构造函数:

  • 默认构造函数(无参,函数体为空);
  • 默认析构函数(无参,函数体为空);
  • 默认拷贝构造函数,对属性进行拷贝。

构造函数的调用规则如下:

  • 如果用户定义有参数构造函数,编译器不再提供默认无参构造,但是会提供拷贝构造;
  • 如果用户定义拷贝构造函数,编译器不再提供其他构造函数。
    这里就不再新的举例子了,利用之前的例子就可以做一些验证,如果把Person类中的无参构造函数注释掉,发现无法使用无参构造的方法创建一个对象;如果把拷贝构造函数注释掉,发现依然可以使用拷贝构造的方法来创建一个对象;如果只留下一个拷贝构造函数,把有参构造,无参构造全部注释掉, 发现无法采用有参构造,无参构造的方法创建一个对象。

深拷贝与浅拷贝

  • 浅拷贝:简单大赋值拷贝操作,到目前为止的拷贝构造函数都是浅拷贝。
  • 深拷贝:在堆区重新申请空间,进行拷贝操作。
    我们对之前的例子做些修改,定义一个新的指针类型的"身高"属性。
class Person
{
public:
    //有参构造函数
    Person(int age, int height)
    {
        cout << "Person有参构造函数调用:" << endl;
        m_Height = new int(height);
        m_Age = age;
    }
    Person()
    {
        cout << "Person无参构造函数调用:" << endl;
    }
    ~Person() 
    {
        //析构代码,将堆区的数据也做释放操作
        if(m_Height!= NULL)
        {
            delete m_Height;
            m_Height = NULL;
        }
        cout << "Person析构函数调用" << endl;
    }
    int m_Age; //年龄
    int *m_Height; //身高
};

int main(void)
{
    Person p1(18, 170);
    cout << "p1的年龄为:" << p1.m_Age << " 身高为:" << *p1.m_Height << endl;

    Person p2(p1); //浅拷贝带来的问题,堆区内存重复释放。
    cout << "p1的年龄为:" << p1.m_Age << " 身高为:" << *p1.m_Height << endl;
    return 0;
}

此段代码在执行的过程中会出现错误。由于编译器为我们提供的拷贝构造函数是浅拷贝的,在拷贝的过程中会只是简单的一个值的复制,导致p2的m_Height和p1的m_Height指向同一块堆区内存。而栈区是先进后出的,所以当p2相关代码执行完毕后,会先执行析构代码。然后,p1也会执行一次,由于堆区内存已经被释放了,所以这里会报错,重复释放堆区数据。

我们可以手写拷贝构造函数代码:

    Person(const Person &p) //拷贝构造函数
    {
        m_Age = p.m_Age;
        //m_Height = p.m_Height; 编译器默认实现这行代码

        //深拷贝
        m_Height = new int(*p.m_Height);
        cout << "Person拷贝构造函数调用" << endl;
    }

在拷贝构造函数中,我们也在堆区来申请另一块内存空间,来存放相同的内容,这样就避免了重复释放,各自释放各自的堆区内容。

  • 总结,如果属性有堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

初始化列表

与构造函数类似,初始化列表也可以用来除初始化属性。

  • 语法 构造函数(): 属性1(值1),属性2(值2)...{}
class Person
{
public:
    Person() : m_A(10), m_B(20), m_C(30)
    {
        
    }
    int m_A;
    int m_B;
    int m_C;
};

int main(void)
{
    Person p;
    cout << "m_A = " << p.m_A << endl;
    cout << "m_B = " << p.m_B << endl;
    cout << "m_C = " << p.m_C << endl;
    return 0;
}

注意到这中写法有一些局限性,能付的初始值是固定的,所以,我们还可以这样写:

class Person
{
public:
    Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c)
    {

    }
    int m_A;
    int m_B;
    int m_C;
};

int main(void)
{
    Person p(30, 20, 9);
    cout << "m_A = " << p.m_A << endl;
    cout << "m_B = " << p.m_B << endl;
    cout << "m_C = " << p.m_C << endl;
    return 0;
}

这样就比较灵活了,我们可以随意赋初值,并且可以在函数体内写上其他的代码

类作为对象类成员

之前在点圆的关系那里提到过类的嵌套使用,这里我们来深入了解一下:

//手机类
class Phone
{
public:
    Phone(string pName)
    {
        cout << "Phone的构造函数调用" << endl;
        m_PName = pName;
    }
    ~Phone()
    {
        cout << "Phone 的析构函数调用" << endl;
    }
    string m_PName; //手机品牌
};

class Person
{
public:
    //Phone m_Phone = Pname; 隐式转换法 --> Phone m_Phone = Phone(Pname)
    Person(string name, string Pname) : m_Name(name), m_Phone(Pname)
    {
        cout << "Person的构造函数调用" << endl;
    }
    string m_Name; //姓名
    Phone m_Phone; //手机
    ~Person()
    {
        cout << "Person的析构函数调用" << endl;
    }
};

int main(void)
{
    Person p("张三", "苹果MAX");
    cout << p.m_Name << "拿着" << p.m_Phone.m_PName << endl;
    return 0;
}

在Person类里面有一个Phone类的成员,那么在创建Person的时候会先创建一个Phone对象,再创建一个Person对象;而析构的时候恰恰相反,先析构Person对象,再析构Phone对象。

静态成员

静态成员就是在成员变量和成员函数前面加上static关键字。

静态成员变量

  • 所有对象共享同一份数据;
  • 在编译阶段分配内存;
  • 类内声明,类外初始化。
class Person
{
public:
    static int m_A; //类内声明
private:
    static int m_B; 
};

int Person::m_A = 100; //类外初始化
int Person::m_B = 200; //类外初始化

int main(void)
{
    Person p;
    cout << p.m_A << endl; //100
    Person p2;
    p2.m_A = 200;//创建另一个类,将其修改为200
    cout << p.m_A << endl; //200

    //静态成员变量不属于任何一个对象,因此有两种访问方式

    //1、通过对象进行访问
    cout << p.m_A << endl; //100

    //2、通过类名进行访问
    cout << Person::m_A << endl;
    
    //私有的静态成员变量是不可以在类外进行访问的
   // cout<< Person::m_B << endl; //错误写法,类外不可以访问
    return 0;
}

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
class Person
{
public:
    //静态成员函数
    static void func()
    {
        cout << "func的调用:" << endl;
        m_A = 100; //静态成员函数可以访问静态成员变量
        //m_B = 200; //非静态成员变量,不可以访问非静态成员变量,无法区分到底要访问哪一个对象的m_B成员。
    }
    static int m_A; //静态成员变量 类内声明
    int m_B = 200;   //非静态成员变量
private:
    static void func2()
    {
        cout << "func2()的调用" << endl;
    }
};

int Person::m_A = 100; //类外初始化

int main(void)
{
    //静态成员函数也有两个访问方式
    //1、通过对象访问
    Person p;
    p.func();

    //2、通过类名访问
    Person::func();
   //Person::func2(); //类外不可以访问私有静态成员变量
    return 0;
}

静态成员变量无法访问非静态成员函数,原因是静态成员变量是属于某一个被实例化出来的对象的。静态成员函数无法确定是哪一个对象在调用,也就无法确定要修改哪一个对象的成员。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容