C++类和对象:对象的初始化和清理

本文是笔者在C++学习过程中,对类和对象的总结记录,如有不对以及不足的地方,欢迎大家指正!

文中代码学于黑马。


1. 构造函数与析构函数

  在C++当中,提供了一种特殊的公有成员函数构造函数析构函数,用于对对象进行初始化和销毁。

  • 构造函数:主要用于在创建对象时为对象的成员属性赋值,即给对象分配资源
    语法:类名(){}
    构造函数可以有参数,也就是说,可以发生重载。
  • 析构函数:主要用于在对象销毁时释放相应的资源
    语法:~类名(){}
    析构函数不可以有参数,所以不能发生重载。

  一个类,可以有多个构造函数,但只能有一个析构函数,不同构造函数之间通过参数的个数和类型来进行区分。

  对于C++来说,对象的初始化与销毁是必须的,所以即使我们不提供构造函数与析构函数,编译器也会提供默认的构造函数与析构函数,并在对象初始化和销毁时由编译器自动调用,不需要手动调用,且只会调用一次。编译器提供的构造函数与析构函数是空实现

#include <iostream>
#include <string>
using namespace std;

// 1. 构造函数与析构函数
class Person_1{
public:
    // 构造函数
    Person_1(){
        cout<<"Person_1 构造函数"<<endl;
    }

    // 析构函数
    ~Person_1(){
        cout<<"Person_1 析构函数"<<endl;
    }
};

void test_1(){
    // 栈区的数据,会在函数调用结束后立即释放
    Person_1 p1;
}

int main() {
    // 1. 构造函数与析构函数
    test_1();
    return 0;
}

运行结果:


构造函数与析构函数

2.构造函数的分类与调用

2.1 分类

  构造函数可以按参数来分类,也可以按类型来进行分类。

  • 参数
    1. 有参构造
    2. 无参构造(默认构造
  • 类型
    1. 普通构造
    2. 拷贝构造
      在创建对象时,是通过同一个类的已创建的对象来对自己进行初始化,即对原对象进行复制,来初始化本对象。
      语法示例:Object(const Object &o){...}
#include <iostream>
using namespace std;

//2.构造函数的分类与调用
class Person_2{
public:
    //无参构造函数(默认)
    Person_2(){
       cout<<"Person_2 无参构造函数(默认)"<<endl;
    }

    //有参构造函数
    Person_2(int age){
        this->age=age;
        cout<<"Person_2 有参构造函数"<<endl;
    }

    //拷贝构造函数
    Person_2(const Person_2 &p){
        age=p.age;
        cout<<"Person_2 拷贝构造函数"<<endl;
    }

    //析构函数
    ~Person_2(){
        cout<<"Person_2 析构函数"<<endl;
    }

private:
    int age;
};

void test_2_1(){
    Person_2 p1;
    Person_2 p2(10);
    Person_2 p3(p2);

}

int main() {
    //2.构造函数的分类与调用
    test_2_1();
    return 0;
}

运行结果:


构造函数的分类

2.2 调用

  构造函数的调用方式,可以分为以下几类:

  • 括号法
      调用默认构造(无参构造)时,不可以加括号,否则编译器会认为这是函数声明。
  • 显示法
      匿名对象(只调用构造方法而没有给对象命名),会在当前行执行结束后,立即被系统回收,即调用析构函数。
      同时,不要用拷贝构造函数来初始化匿名函数,否则,在编译器看来这是一个对象声明:Object(o1); == Object o1;
  • 隐式转换法
#include <iostream>
using namespace std;

//调用
void test_2_2(){
    // 括号法
    Person_2 p1(10);

    // 调用默认构造(无参构造)时,不可以加括号,否则编译器会认为这是函数声明。
    // Person_2 p2();  // Function 'p2' is never used

    // 显示法
    Person_2 p2=Person_2(10);   // 有参
    Person_2 p3=Person_2(p2);       // 拷贝
    // Person_2(10) 单独写即为匿名对象,在当前行执行结束后,立即被释放(析构)
    
    // 不要用拷贝构造函数来初始化匿名函数,否则,在编译器看来这是一个对象声明
    // Person_2(p4);   // 等价于 Person_2 p4
    
    // 隐式转换法
    Person_2 p4=10; // 等价于 Person_2 p2=Person_2(10);
    Person_2 p5=p4; // 等价于 Person_2 p3=Person_2(p4);
}


3. 拷贝函数的调用时机

  在C++中什么时候会调用拷贝构造函数呢?有以下三种情况:

  1. 一个对象需要通过一个已经创建的对象来初始化
  2. 一个对象以值传递的方式传入函数体
  3. 一个对象以值传递的方式从函数返回
#include <iostream>
using namespace std;

class Person_2{
public:
    //无参构造函数(默认)
    Person_2(){
       cout<<"Person_2 无参构造函数(默认)"<<endl;
    }

    //有参构造函数
    Person_2(int age){
        this->age=age;
        cout<<"Person_2 有参构造函数"<<endl;
    }

    //拷贝构造函数
    Person_2(const Person_2 &p){
        age=p.age;
        cout<<"Person_2 拷贝构造函数"<<endl;
    }

    //析构函数
    ~Person_2(){
        cout<<"Person_2 析构函数"<<endl;
    }

private:
    int age;
};

// 3.拷贝函数的调用时机
// 3.1 一个对象需要通过一个已经创建的对象来初始化
void test_3_1(){
    Person_2 p(10); // 无参构造函数
    Person_2 p1(p);     // 调用拷贝构造函数
    Person_2 p2=p;      // 调用拷贝构造函数

    Person_2 p3;
    p3=p;   // 不调用拷贝构造函数,是赋值操作
}

// 3.2 一个对象以值传递的方式传入函数体
void value(Person_2 p){}
void test_3_2(){
    Person_2 p; // 无参构造函数
    value(p);
}

// 3.3 一个对象以值传递的方式从函数返回
Person_2 result(){
    Person_2 p;
    cout<<"result()函数内变量p的地址为:\t\t"<<(int *)&p<<endl;
    return p;
}
void test_3_3(){
    Person_2 p=result();
    cout<<"result()函数返回的变量p的地址为:\t"<<(int *)&p<<endl;
}

int main() {
    // 3.拷贝函数的调用时机
    // 3.1 一个对象需要通过一个已经创建的对象来初始化
    test_3_1();
    cout<<"===================="<<endl;
    // 3.2 一个对象以值传递的方式传入函数体
    test_3_2();
    cout<<"===================="<<endl;
    // 3.3 一个对象以值传递的方式从函数返回
    test_3_3();
    cout<<"===================="<<endl;

    return 0;
}

运行结果:


image.png

4. 构造函数的调用规则

  在默认的情况下,C++编译器会至少给类添加以下三个函数:

  1. 默认构造函数:无参,函数体为空
  2. 默认析构函数:无参,函数体为空
  3. 默认拷贝构造函数:对对象的属性进行简单的值拷贝

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

  1. 若用户自定义了有参构造函数,那么C++就不会再提供默认无参构造函数,而仅提供默认拷贝构造函数。
  2. 若用户自定义了拷贝构造函数,那么C++就不会再提供其他的构造函数。

5. 深拷贝与浅拷贝

  深拷贝与浅拷贝,一直是C++面试的一个热点与难点,那么这两者的区别是什么呢?

  • 浅拷贝:在对象复制时,仅进行简单的赋值拷贝
  • 深拷贝:在对象复制时,会在堆区重新申请空间,再进行拷贝操作。

  在默认拷贝构造函数中,进行的是浅拷贝,而浅拷贝会将新旧对象的指针型成员变量指向同一块内存。因此在一个对象被销毁(调用析构函数)后,这一块内存就已经被释放了,所以当另一个对象调用析构函数时,就会造成对同一内存的重复释放,从而引发错误。所以如果有在堆区开辟的属性,就一定要自定义拷贝构造函数,通过深拷贝来进行拷贝

#include <iostream>
using namespace std;

// 5. 深拷贝与浅拷贝
class Person_3{
public:
    //无参构造函数(默认)
    Person_3(){
        cout<<"Person_3 无参构造函数(默认)"<<endl;
    }

    //有参构造函数
    Person_3(int age,int height){
        this->age=age;
        this->height=new int(height);
        cout<<"Person_3 有参构造函数"<<endl;
    }

    //拷贝构造函数
    Person_3(const Person_3 &p){
        age=p.age;
        //利用深拷贝来在堆区开辟空间,避免利用浅拷贝带来的堆区重复释放问题
        height=new int(*p.height);
        cout<<"Person_3 拷贝构造函数"<<endl;
    }

    //析构函数
    ~Person_3(){
        //释放在堆区开辟的空间
        if(height!=NULL){
            delete height;
        }
        cout<<"Person_3 析构函数"<<endl;
    }

    void show(){
        cout<<"age:"<<age<<",height:"<<*height<<endl;
        cout<<"height变量的地址:"<<height<<endl;
    }

private:
    int age;
    int *height;
};
void test_5(){
    Person_3 p1(22,180);
    Person_3 p2(p1);
    cout<<"p1:"<<endl;
    p1.show();
    cout<<"p2:"<<endl;
    p2.show();
}


int main() {
    // 5. 深拷贝与浅拷贝
    test_5();

    return 0;
}

运行结果:


image.png

6. 初始化列表

  C++除了提供传统的构造函数之外,还提供了初始化列表语法,用来初始化属性。

  • 语法:构造函数():属性1(值1),属性2(值2)...{...}
#include <iostream>
using namespace std;

// 6. 初始化列表
class Person_4{
public:

    // 初始化列表方式初始化
    Person_4(int a,int b,int c):A(a),B(b),C(c){}

    void show(){
        cout<<"A:"<<A<<",B:"<<B<<",C:"<<C<<endl;
    }

private:
    int A;
    int B;
    int C;

};
void test_6(){
    Person_4 p(1,2,3);
    p.show();
}

int main() {
    // 6. 初始化列表
    test_6();
    return 0;
}

7. 类对象作为类成员

  在C++中,一个类的成员可以是另一个类的对象,我们称这种成员为对象成员

  eg:

class A{}
class B{
    A a;    // 对象成员
}

  带有对象成员的类,在初始化时,先构造对象成员,再构造自己;在销毁时,先析构自己,再析构对象成员,两者顺序相反。

#include <iostream>
using namespace std;

// 7. 类对象作为类成员
class Pat{
public:
    Pat(string name){
        this->name=name;
        cout<<"Pat 有参构造函数"<<endl;
    }

    ~Pat(){
        cout<<"Pat 析构函数"<<endl;
    }
private:
    string name;
};
class Person{
public:
    string name;
    Pat pat;

    // 初始化列表传参
    Person(string person,string patname):name(person), pat(patname){
        cout<<"Person 有参构造函数"<<endl;
    }

    ~Person(){
        cout<<"Person 析构函数"<<endl;
    }
};
void test_7(){
    Person p("Alice","cat");
}

int main() {
    // 7. 类对象作为类成员
    test_7();
    return 0;
}

运行结果:


image.png

8. 静态成员

  类的成员变量与成员方法,在前面加上关键字static后。就被称为静态成员,所以静态成员可以分为静态成员变量静态成员函数

  这两者的访问,都可以通过以下两种方式达成:

  • 通过对象:对象名.静态成员变量名/静态成员函数名
  • 通过类名:类名::静态成员变量名/静态成员函数名

8.1 静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
#include <iostream>
using namespace std;

// 8. 静态成员
// 静态成员变量
class Person_5{
public:
    static int sA;
private:
    static int sB;
};
//类内声明,类外初始化
int Person_5::sA=10;
int Person_5::sB=20;
void test_8_1(){
    Person_5 p1;
    //1. 通过对象访问
    p1.sA=100;
    cout<<"p1.sA="<<p1.sA<<endl;

    Person_5 p2;
    p2.sA=200;  //所有对象共享同一份数据
    cout<<"p1.sA="<<p1.sA<<endl;
    cout<<"p2.sA="<<p2.sA<<endl;

    //2. 通过类名访问
    cout<<"Person_5::sA="<<Person_5::sA<<endl;

}

int main() {
    // 8. 静态成员
    // 静态成员变量
    test_8_1();
    return 0;
}

运行结果:


image.png

8.2 静态成员函数

  • 所有对象共享同一个函数
  • 静态成员方法仅能访问静态成员变量。如若不然,某个对象在调用静态成员方法时,不能确认方法中的普通变量属于哪个对象。
#include <iostream>
using namespace std;

class Person_6{
public:
    static int sA;

    static void func(){
        cout<<"static void func()"<<endl;
        sA=200;
    }

};
//类内声明,类外初始化
int Person_6::sA=10;
void test_8_2(){
    Person_6 p;
    //1. 通过对象访问
    p.func();
    //2. 通过类名访问
    Person_6::func();
}

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

推荐阅读更多精彩内容