目录

C编程进阶阶段4.2对象

C++编程:进阶阶段—4.2对象


4.2 对象特征

对象的初始化和清理:C++中,每个对象都有初始设置和对象销毁前的清理数据的设置。

4.2.1 构造函数和析构函数

C++中利用构造函数和析构函数对对象进行初始化和清理。这两个函数会被编译器自动调用,完成对象的初始化和清理。如果程序员不提供构造和析构,编译器会提供构造函数和析构函数,但是是空的。

构造函数: 创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

语法:类名(){}

1.构造函数,没有返回值也不写void;

2.函数名与类名相同;

3.构造函数可以有参数,因此可以发生重载;

4.程序在调用对象时会自动调用构造,无须手动调用,且只调用一次。

析构函数: 对象销毁前系统自动调用,执行一些清理工作。

语法:~类名(){}

1.析构函数,没有返回值也不写void;

2.函数名与类名相同,在函数名前加上符号~;

3.析构函数不可以有函数,因此不可以发生重载;

4.程序在对象销毁前会自动调用析构,无须手动调用,且只调用一次。

代码如下:

#include <iostream>
using namespace std;

class Person
{
public:
    //构造函数 初始化对象
    Person()
    {
        cout<<"Person构造函数的调用"<<endl;
    }

    //析构函数 销毁/清理对象
    ~Person()
    {
        cout<<"Person析构函数的调用"<<endl;
    }
};

void test01()
{
    Person p;//栈上的数据,该函数执行完后,p这个对象会被释放
}

int main()
{
    //对象的初始化和清理

    test01();
    Person p;

    system("pause");
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/e0608a3fd0364afcafb3eee6ef3e5aa1.png

4.2.2 构造函数的分类

按参数分:有参构造、无参构造;

按类型分:普通构造、拷贝构造。

调用方式:括号法、显示法、隐式转换法。

代码如下:

#include <iostream>
using namespace std;

//构造函数发分类及调用
class Person
{
public:
    //无参(默认)构造
    Person()
    {
        cout<<"Person的无参构造函数的调用"<<endl;
    }

    //有参构造
    Person(int a)
    {
        age=a;
        cout<<"Person的有参构造函数的调用"<<endl;
    }

    //拷贝构造函数(将Person p的属性拷贝过来)
    Person(const Person &p)
    {
        age=p.age;
        cout<<"Person的拷贝构造函数的调用"<<endl;
    }

    ~Person()
    {
        cout<<"Person析构函数的调用"<<endl;
    }

private:
    int age;
};

void test01()
{
    //调用:括号法
    cout<<"括号法调用构造函数:"<<endl;
    Person p1;//默认构造函数调用,不用加括号,编译器会认为Person p1();是一个函数声明。
    Person p2(21);//有参构造函数调用
    Person p3(p2);//拷贝构造函数调用

    //调用:显示法
    cout<<"显示法调用构造函数:"<<endl;
    Person p4;
    Person p5=Person(21);//有参构造
    Person P6=Person(p5);//拷贝构造

    Person(21);//表示一个匿名对象,在等式左边的P2就是给他取的名字,匿名对象执行后会立即回收。
    cout<<"匿名对象清理后执行了这句代码"<<endl;

    //PS:不要用拷贝构造 初始化匿名对象,编译器会认为Person(p3);是一个对象声明Person p3;
    //Person(p3);


    //调用:隐式转换法
    cout<<"显示法调用构造函数:"<<endl;
    Person p7=21;//有参构造
    Person p8=p7;//拷贝构造
}

int main()
{
    test01();
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/bafebe31ee1041e1a42924eabbdaee7a.png

4.2.3 拷贝函数调用时机

  • 使用一个已经创建完毕的对象来初始化一个新对象;
  • 值传递的方式给函数参数传值;
  • 以值方式返回局部对象。

代码如下:

#include <iostream>
using namespace std;

//拷贝构造函数调用时机
class Person
{
public:
    //无参(默认)构造
    Person()
    {
        cout<<"Person的无参构造函数的调用"<<endl;
    }

    //有参构造
    Person(int a)
    {
        age=a;
        cout<<"Person的有参构造函数的调用"<<endl;
    }

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

    ~Person()
    {
        cout<<"Person析构函数的调用"<<endl;
    }

    int age;
};

//使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
    cout<<"test01函数调用"<<endl;
    Person p1(21);
    Person p2(p1);
    cout<<"p2的年龄为:"<<p2.age<<endl;
}

//值传递的方式给函数参数传值
void doWork(Person p)
{

}

void test02()
{
    cout<<"test02函数调用"<<endl;
    Person p;
    doWork(p);//这里传入的p和dowork中的p不一样
}

//以值方式返回局部对象
Person doWork2()
{
    Person p1;
    cout<<"p1的地址为:"<<(long long)&p1<<endl;
    return Person(p1);//直接返回p1则不会调用拷贝函数,因为编译器自动做了优化(可以看到p1和p的地址一样)
}

void test03()
{
    cout<<"test03函数调用"<<endl;
    Person p=doWork2();
    cout<<"p的地址为:"<<(long long)&p<<endl;
}

int main()
{
    test01();

    test02();

    test03();

    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/a7b437701c1e4bbdb414ba98a5f93d66.png

4.2.4 构造函数调用规则

默认情况下,C++编译器至少给类添加三个函数;

  1. 1.默认构造函数(无参,函数体为空)
  2. 2.默认析构函数(无参,函数体为空)
  3. 3.默认拷贝构造函数,对属性进行值拷贝

调用规则:

  • 如果用户定义了有参构造函数,则编译器不提供默认无参构造,但会提供默认拷贝构造
  • 如果用户定义了拷贝构造函数,则编译器不再提供其他构造函数

代码如下:

#include <iostream>
using namespace std;

class Person
{
public:
    // Person()
    // {
    //     cout<<"person的默认构造函数"<<endl;
    // }
    Person(int a)
    {
        age=a;
        cout<<"Person的有参构造函数的调用"<<endl;
    }
    // Person(const Person &p)
    // {
    //     age=p.age;
    //     cout<<"Person的拷贝构造函数的调用"<<endl;
    // }

    ~Person()
    {
        cout<<"Person析构函数的调用"<<endl;
    }

    int age;
};

// void test01()
// {
//     Person p;
//     p.age=18;

//     Person p2(p);
//     cout<<"p2的年龄为:"<<p2.age<<endl;
// }

void test02()
{
    Person p(28);
    Person p2(p);
    cout<<"p2的年龄为:"<<p2.age<<endl;
}

int main()
{
    //test01();
    test02();
    return 0;
}

输出如下:用户定义了拷贝构造函数

https://i-blog.csdnimg.cn/direct/992ece767c5347ef8cc3962b3dbe0515.png

输出如下:用户没有定义拷贝构造函数

https://i-blog.csdnimg.cn/direct/655ff66a4f8a4c8b9054d72026b86a66.png

错误示例:用户定义了有参构造,但没有定义无参(默认)构造,则编译器也不会提供默认构造,此时调用默认构造则会报错。

https://i-blog.csdnimg.cn/direct/1a5da3b85937458694afe302f5edc356.png

输出如下:用户只定义了有参构造,则编译器依然或提供拷贝构造

https://i-blog.csdnimg.cn/direct/cbee136b6dd640b1bdbe0f7f6a1f5427.png

4.2.5 深拷贝与浅拷贝

浅拷贝:编译器提供的拷贝函数,简单的赋值拷贝操作;

缺点:容易导致堆区的重复释放,利用深拷贝解决。

深拷贝:在堆区重新申请空间,进行拷贝操作,而不是与被拷贝的指针指向相同的空间。

PS:如果属性有在堆区开辟的,一定要自己定义拷贝构造函数,防止浅拷贝中出现的问题。

代码如下:

#include <iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        cout<<"person的默认构造函数"<<endl;
    }
    Person(int a,int h)
    {
        age=a;
        height=new int(h);
        cout<<"Person的有参构造函数的调用"<<endl;
    }
    //自己实现拷贝构造函数,解决浅拷贝的问题
    Person(const Person &p)
    {
        age=p.age;
        height=p.height;//编译器写的(浅拷贝)
        height= new int(*p.height);//深拷贝操作,另外开辟空间
        cout<<"Person的拷贝构造函数的调用"<<endl;
    }

    ~Person()
    {
        //析构的作用,将堆区new的数据手动释放
        if(height!=NULL)//若指针不为空,则需要释放
        {
            delete height;//P2先释放,完了之后P也需要释放,但两个对象的指针操作的是同一个堆区中的地址,造成重复释放的非法操作,因此会报错
            height=NULL;//防止野指针出现,将指针置空
        }
        cout<<"Person析构函数的调用"<<endl;
    }

    int age;
    int * height;
};

void test01()
{
    Person p(28,160);

    Person p2(p);
    cout<<"p2的年龄为:"<<p2.age<<" 身高为:"<<*p2.height<<endl;
}

int main()
{
    test01();

    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/8fa3878f7b8e4b6caf37511b8e06b657.png

4.2.6 初始化列表

作用:C++提供了初始化列表语法,用来初始化属性。

语法:构造函数():属性1(值1),属性2(值2)…{}

代码如下:

#include <iostream>
using namespace std;

class Person
{
public:
    //传统初始化操作
    // Person(int a,int b,int c)
    // {
    //     A=a;
    //     B=b;
    //     C=c;
    // }

    //初始化列表赋初值
    //Person():A(1),B(2),C(3){}
    Person(int a,int b,int c):A(a),B(b),C(c){}
    int A;
    int B;
    int C;
};

void test01()
{
    //Person p(10,20,30);//传统赋值
    Person p(1,2,3);//列表赋值
    cout<<"A="<<p.A<<endl;
    cout<<"B="<<p.B<<endl;
    cout<<"C="<<p.C<<endl;

}

int main()
{
    test01();
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/ab8c8c8902a24e64b396fe97a1b93d72.png

4.2.7 类对象作为类成员

C++中类的成员可以是另一个类的对象,称为对象成员。

注意对象作为成员时,两种对象的构造和析构函数的顺序。(先构造其他类,再构造本类,先析构本类,再析构其他类)

代码如下:

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

//对象成员
class Phone
{
public:
    //手机品牌
    string PName;
    Phone(string pname)
    {
        cout<<"Phone的构造函数的调用"<<endl;
        PName=pname;
    }

    ~Phone()
    {
        cout<<"Phone析构函数的调用"<<endl;
    }
};

class Person
{
public:
    //P(pname)相当于Phone P=pname; 隐式转换法
    Person(string name,string pname):Name(name),P(pname)
    {
        cout<<"Person的构造函数的调用"<<endl;
    }


    ~Person()
    {
        cout<<"Person析构函数的调用"<<endl;
    }

    string Name;
    Phone P;
};

void test01()
{
    Person p("张三","iPhone18");

    cout<<p.Name<<"拿着"<<p.P.PName<<endl;
}



int main()
{
    test01();

    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/320d64ccac6b4478a3cfb5617b1ce384.png

4.2.8 静态成员

静态成员是指在成员变量和成员函数卡加static关键字,静态成员都有三种访问权限。

静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存(程序运行前)
  • 类内声明,类外初始化

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

代码如下:

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

//静态成员
class Person
{
public:
    //静态成员变量
    static int A;//类内声明
    int B;

    //静态成员函数
    static void func()
    {
        A=44;
        //B=22;//静态成员函数访问非静态成员变量,报错,无法区分是哪个对象的B
        cout<<"静态成员函数调用"<<endl;
    }
};

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

void test01()
{
    Person p;
    cout<<p.A<<endl;

    Person p2;
    p2.A=200;
    
    //所有对象共享同一份数据,因此有两种访问方式:通过对象访问;通过类名访问
    cout<<p.A<<endl;
    cout<<Person::A<<endl;
}

void test02()
{
    //两种访问方式:通过对象访问;通过类名访问
    Person p;
    p.func();

    Person::func();
    cout<<p.A<<endl;
}

int main()
{
    test01();
    test02();
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/1afd6d59d4634b8e90c376c4ff3493e4.png

错误示例:静态成员函数访问非静态成员变量

https://i-blog.csdnimg.cn/direct/680b25935b1c44339927231d07c312cb.png

4.2.9 成员变量和成员函数的存储

类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上的。

代码如下:

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

//静态成员
class Person1
{

};

class Person2
{
    int A;//非静态成员变量 
};

class Person3
{
    int A;
    static int B;//静态成员变量
};

int Person3::B=9;

class Person4
{
    int A;
    static int B;
    void func(){}//非静态成员函数
};

class Person5
{
    int A;
    static int B;
    void func(){}//非静态成员函数
    static void func2(){};
};

void test01()
{
    Person1 p1;
    //空对象占用内存为1,为了区分空对象占内存的位置,每个空对象有一个唯一的地址
    cout<<"size of p1="<<sizeof(p1)<<endl;

    Person2 p2;
    //有非静态成员变量,占4字节  属于类的对象上的数据
    cout<<"size of p2="<<sizeof(p2)<<endl;

    Person3 p3;
    //有静态成员变量  不属于类的对象上的数据
    cout<<"size of p3="<<sizeof(p3)<<endl;

    Person4 p4;
    //非静态成员函数  不属于类的对象上的数据
    cout<<"size of p4="<<sizeof(p4)<<endl;

    Person5 p5;
    //静态成员函数  不属于类的对象上的数据
    cout<<"size of p5="<<sizeof(p5)<<endl;
}

int main()
{
    test01();

    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/9efcefc1214c48848b5ab04665ed977e.png

4.2.10 this指针

每一个非静态成员函数只会产生一个函数实例,所有同类中的多个对象会公用一块代码。

C++提供this指针来指向被调用的成员函数所属的对象。this指针是隐含每一个非静态成员函数内的一种指针,不需要定义,直接使用即可。

用途:

  • 当形参和成员变量同名时,用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this。

PS:用Person&定义返回值类型,是因为可以一直对同一个空间操作,用Person定义返回值类型表示值返回,会复制一份新的数据(按照本体p2创建了新的数据,而不是返回的p2本体),调用了拷贝构造函数。

代码如下:

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

class Person
{
public:
    int age;
    Person(int age)
    {
        //age=age;//报错
        //this指针指向被调用的成员函数所属的对象p1
        this->age=age;
    }
//用Person&定义返回值类型,是因为可以一直对同一个空间操作,用Person定义返回值类型表示值返回,会复制一份新的数据(按照本体p2创建了新的数据,而不是返回的p2本体),调用了拷贝构造函数
     Person& PersonAddAge(Person &p)
     {
        this->age+=p.age;
        //this指向p2的指针,*p2指向p2本体
        return *this;
     }
};

//解决名称冲突
void test01()
{
    Person p1(18);
    cout<<p1.age<<endl;
}

//用*this 返回对象本身
void test02()
{
    Person p1(31);

    Person p2(31);
    p2.PersonAddAge(p1);
    cout<<p2.age<<endl;
    p2.PersonAddAge(p1).PersonAddAge(p1);//用this*返回才能链式追加
    cout<<p2.age<<endl;

}

int main()
{
    test01();
    test02();
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/463463df00b54cd48ae46216a151acac.png

错误示例:名称冲突,形参和属性名相同时,不能输出正确结果

https://i-blog.csdnimg.cn/direct/3f6235c6989a4277bce228586780c1d8.png

4.2.11 空指针访问成员函数

C++中空指针可以调用成员函数,但需要注意有没有用this指针。如果用到this指针,需要加以判断保证代码的健壮性。

代码如下:

#include <iostream>
using namespace std;

//空指针调用成员函数
class Person
{
public:
    void showClassName()
    {
        cout<<"this is person class"<<endl;
    }

    void showPersonAge()
    {
        if(this==NULL)
        {
            return;
        }
        //传入指针为空,报错  在前面加一个空指针的判断
        cout<<"age="<<this->age<<endl;
    }

    int age;
};

void test01()
{
    Person *p=NULL;
    p->showClassName();
    p->showPersonAge();
}

int main()
{
    test01();
    return 0;
}

输出如下:

https://i-blog.csdnimg.cn/direct/884bc59d7d344dd6a54846e843d51105.png

错误示例:用空指针访问属性,图中age,默认是this->age,而访问时用的空指针,this为空所以不能指向正确的对象的属性。

https://i-blog.csdnimg.cn/direct/0bdb5eb5bf9b42f1bade770e7118205a.png

4.2.12 const修饰成员函数

常函数:

  • 成员函数后加const后称为常函数;
  • 常函数内不可用修改成员属性;
  • 成员属性声明时加关键词mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象;
  • 常对象不允许修改指针指向的值;
  • 常对象只能调用常函数

代码如下:

#include <iostream>
using namespace std;

//常函数
class Person
{
public:
    //this指针的本质是一个指针常量Person * const this 指针的指向是不可修改的
    //后面加的const相当于const Person * const this,使this指向的值也不可修改
    void showPerson() const
    {
        this->b=99;
        //this=NULL;//this的指针指向不能修改
        cout<<"this is person class"<<endl;
    }
    Person(){}//不写默认构造函数会报错实例化的常对象没有初始化

    void func(){}
    int age;
    mutable int b;
};

void test01()
{
    Person p;
    p.showPerson();
}

void test02()
{
    const Person p;
    //p.age=99;//报错 常对象不允许修改指针指向的值
    p.b=88;
    p.showPerson();
    //p.func();//报错 常对象不能调用非常函数
}

int main()
{
    test01();
    test02();
    return 0;
}