猿创征文 |【C++】面向对象之微观部分——类的组成(中)

本文详细介绍了C++中的常对象与常函数,包括它们的定义、语法关系和意义,以及如何使用`mutable`关键字。接着探讨了类中的静态成员函数,解释了静态成员变量和静态成员函数的特性和使用场景。此外,文章深入讲解了拷贝构造的概念,分析了深拷贝和浅拷贝的区别,并通过示例展示了如何处理堆内存的对象拷贝问题。最后,讨论了匿名对象的产生和生命周期。
摘要由CSDN通过智能技术生成

前言

打算做一个C++知识体系的专栏,干货较多,整理较慢,会定期产出,想学习可以关注一下。

本文承接上篇文章面向对象之微观部分——类的组成(上)感兴趣的可以了解一下。

一、常对象与常函数

1.1 常对象与常函数的定义形式

常对象: 就是const修饰的类对象,常引用也是常对象。

const + 类型(引用类型) 对象名(构造函数的实参表);
类型(引用类型) const + 对象名(构造函数的实参表);
常对象:只能以读取的方式访问类中的属性,而不能修改类中的属性。
起到作用:保护与安全机制。

注:

  1. 常对象只能调用常成员函数;
  2. 非常对象优先调用非常成员函数,如果没有,也可以调用常成员函数;
  3. 如果一个普通对象被常引用给引用了,那么通过这个对象本身去访问成员函数是通过非常对象的方式访问,而通过引用访问成员函数是用过常对象的方式访问。

常函数: 就是const修饰的类中的成员函数就叫常函数。

class  类名
{
    //类中的成员函数 + const修饰
    返回值  函数名(形参列表)const
    {
        //常成员函数,简称常函数。    
    }
};

注:

  1. 在常成员函数中,不允许修改成员变量的值;
  2. 常成员函数可以重载,常成员函数和非常成员函数也构成重载关系
  3. 常成员函数的const本质上修饰的是 this 指针,
    this 指针本来应该是 : 类名 * const this;
    常成员函数的this指针应该是:const 类名 * const this;

常函数的特点是:
在常函数中只能以读取类中属性的方式进行访问,而不能对类中的属性进行写操作。
常函数的意义:
保护类中的属性或形参变量不会在常函数体内被修改,起到一种保护作用。

mutable关键字:mutable关键字修饰的成员变量允许在常成员函数中修改;

代码示例:

#include <iostream>
using namespace std;

class Student
{
private:
    string name;
    int age;
    mutable int id;//允许在常函数中修改
public:
    Student(string _name,int _age,int _id):name(_name),age(_age),id(_id)
    {

    }
    void show()const
    {
        cout << "常成员函数" << endl;
        //常成员函数不能修改成员变量的值
        //this->age=100;错误的
        this->id=1003;//可以修改因为有mutable修饰
        cout << "姓名:" << name << " 年龄:" << age <<" 学号:" << id << endl;
    }
    //与上面构成重载关系
    void show()
    {
        cout << "普通成员函数" << endl;
        cout << "姓名:" << name << " 年龄:" << age <<" 学号:" << id << endl;
    }

};
int main()
{
    Student s1("张三",20,1001);
    s1.show();
    const Student s2("夜猫徐",20,1002);
    s2.show();
    const Student& a=s1;
    a.show();//通过引用访问成员函数

    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 在常成员函数中,不允许修改成员变量的值,mutable关键字修饰的成员变量允许在常成员函数中修改。
  2. 常成员函数和非常成员函数也构成重载关系。
  3. 一个普通对象被常引用给引用了,通过引用访问成员函数是用过常对象的方式访问。

1.2 常对象与常函数语法关系与意义

语法关系:
常对象,只能调用常函数,而不能调用普通函数。因为普能函数有写的属性。
普通对象,当然可以调常函数。

代码验证:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
public:
    Stu(string _name,int _age):name(_name),age(_age)
    {
        cout << "Stu中的有参构造" << endl;
    }
    ~Stu()
    {
        cout << "Stu的析构" << endl;
    }
    int getage()const//常函数
    {
        //this->age=100;这种方式报错
        //常函数是不能修改类中的属性的
        return this->age;
    }
    string getname()const
    {
        //this->name="lisi";
        return this->name;
    }
};
bool compare(const Stu& stu1,const Stu& stu2)
{
    //常对象只能调用常函数。
    return stu1.getage()>stu2.getage();
}
int main()
{
    Stu stu1("zhangsan",20);
    Stu stu2("yemaoxu",18);
    if(compare(stu1,stu2))
    {
        cout << "zhangsan大" << endl;
    }
    else
    {
        cout << "yemaoxu大" << endl;
    }
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 常对象只能调用常函数,不可以调用普通函数,因为普通函数有修改类中属性的权限。
  2. 常函数只能读取类中的成员属性,而不可以修改类中成员函数。
    普通对象是可以调用常函数的。

常对象与常函数在程序设计中的意义:

const修饰的常对象及常函数起到了对,对象中的数据的保护,使大型工程代码更健壮更安全。

二、类中静态成员函数

2.1 回顾静态成员变量的特点

我们知道static修饰的成员对象,是对成员对象存储形式的修饰,他存储在静态区。
必须在类中声明,类外初始化,才能定义空间。

所以他具有以下特征:

  1. 该静态成员对象是属于类的,而非某个对象的。
  2. 他不依赖于某个对象,可以直接使用类名 + :: 域名访问符直接访问。
  3. 该静态成员对象为类的所有对象所共享一份。
  4. 该静态成员不占用类对象的空间。

2.2 类中的static修饰的静态成员函数

加了static修饰的成员函数,他不并是修饰函数的存储形式,是修饰的他的函数级别。

static修饰的成员函数变成了一个全局函数,只不过是隐藏在类的作用域之中而已。
当这个成员函数被升级为全局函数之后,类中对象在堆上或在栈上什么时候产生就与之没有关系了,因为它没有了this指针。
且因为静态成员函数没有了this指针,所以无法访问类中非静态的属性,只能访问类中的静态属性
所以静态成员函数也具有静态成员变量的特性,他不依赖于某个对象的调用,他是为整个类而服务,而非某个对象

代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
    
public:
    static int count;
    Stu(string name, int age)
    {
        this->name = name;
        this->age = age;
        count++;
    }
    static void getCount()
    {
        //cout << name <<endl;错误,静态成员函数没有this指针
        cout << count <<endl;//只能访问静态成员变量
    }
};
//类中静态成员变量类外进行初始化的方式:
int Stu::count;

int main()
{
    Stu stu1("yemaoxu",18);
    Stu stu2("zhangsan",20);
    stu1.getCount();//访问静态成员函数的方式1:通过类对象访问
    Stu::getCount();//访问静态成员函数的方式2:通过类名直接访问
    //cout << Stu::count << endl;//私有属性时不可以访问,公有时可以通过类对象访问
    cout << stu1.count << endl;//静态成员变量的访问方式1:在静态成员变量是公有时可以通过类对象访问
    Stu::count=5;//静态成员变量的访问方式2:在静态成员变量是公有时通过类名直接访问
    cout << Stu::count << endl;
    stu1.count=10;//通过stu1修改count,通过stu2访问也会发生变化
    cout << stu2.count << endl;

    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 静态成员变量的访问方式1:在静态成员变量是公有时可以通过类对象访问。
    静态成员变量的访问方式2:在静态成员变量是公有时可以通过类名直接访问。
  2. 访问静态成员函数的方式1:公有时通过类对象访问。
    访问静态成员函数的方式2:公有时通过类名直接访问。
  3. 静态成员函数没有this指针,只能访问静态成员变量。
  4. 类中静态成员变量类外进行初始化的方式:int Stu::静态变量名;

为什么要使用静态成员变量?
为了让成员变量的存在不依赖于任何类对象。
为什么要使用静态成员函数?
为了让函数逻辑的执行不依赖于任何类对象。

三、拷贝构造与深浅拷贝

3.1 编译器默认提供的拷贝构造格式

函数名:与类同名
返回值:没有
形参表:const 类名 &

类名(const 类名& other)
{
    this->属性  = other.属性;
    ...
}

3.2 拷贝构造的调用时机

  1. 当定义本对象,使用已经存在的对象进行初始化时,编译器自动调用拷贝构造。
  2. 当函数参数为类类型时,则自动调用类中的拷贝构造。
  3. 当函数返回值为类类型时,则自动调用类中的拷贝构造。

3.3 拷贝构造之深浅拷贝

编译器会默认使用浅拷贝,第一性能方面浅拷贝性能更高,第二,编译器没有考虑到类中有属性指针指向堆区的情况。
如果有类中属性指针指向堆区,那么就必须进行深拷贝的改造。

3.3.1 浅拷贝

代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
public:
    //有参构造
    Stu(string name, int age)
    {
        this->name = name;
        this->age = age;
    }
    //编译器给我提供默认拷贝构造语法形式:这种简单的值的传递也称之为浅拷贝。浅拷贝对计算机性能消耗较低。
    Stu(const Stu& other):name(other.name),age(other.age)//拷贝构造函数也可以有初始化表
    {
        //this->name=other.name;
        //this->age=other.age;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age << endl;
    }
};
int main()
{
    Stu s1("夜猫徐",18);
    s1.show();
    //显式调用类中拷贝构造,当类中并没有提供这个构造,编译器会自动给生成一个拷贝构造.
    //这个编译器会自动给生成的拷贝构造就是上面我们写的拷贝构造语法形式实现的。
    Stu s2(s1);
    s2.show();
    //隐式调用类中的拷贝构造。
    Stu s3 = s1;
    s3.show();
    Stu *p=new Stu(s1);
    p->show();
    delete p;
    p=nullptr;
    return 0;
}
}

结果展示:
在这里插入图片描述
总结:

  1. 这种简单的值的传递也称之为浅拷贝。浅拷贝对计算机性能消耗较低。
  2. 编译器会给我提供默认拷贝构造,如果你不自己提供,则默认的是浅拷贝。
  3. 隐式调用类中的拷贝构造Stu s3 = s1;
  4. 显式调用类中拷贝构造Stu s2(s1);
  5. 显示和隐式调用在没有自己提供拷贝构造也是可以实现的,因为调用了默认的拷贝构造。
  6. 拷贝构造函数也可以有初始化表。

3.3.2 深拷贝

有类中属性指针指向堆区,那么就必须进行深拷贝的改造。

改造代码示例:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
    int *p;
public:
    //有参构造
    Stu(string name, int age)
    {
        this->name = name;
        this->age = age;
        this->p=new int[20];//有类中属性指针指向堆区,升级为深拷贝
        cout << "Stu的构造"  << endl;
    }
    //编译器给我提供默认拷贝构造语法形式:这种简单的值的传递也称之为浅拷贝。浅拷贝对计算机性能消耗较低。
    Stu(const Stu& other)/*:name(other.name),age(other.age)*/
    {
        this->name=other.name;
        this->age=other.age;
        //指针成员也会只做简单的赋值,相当于两个对象的指针成员指向的是同一块空间,调用析构函数释放时,
        //就会出现 double free的问题不能简单的使用浅拷贝,要升级为深拷贝
        //深拷贝实现步骤
        //1.开辟新空间
        this->p=new int[20];
        //2.拷贝数据
        //memcpy(this->p, other.p,sizeof(int[20]));
        //memove针对小型嵌入设备的开发。
        memmove(this->p,other.p,sizeof(int[20]));//memmove有一种安全机制。
        cout << "Stu的拷贝构造" << endl;
    }
    ~Stu()
    {
        delete []p;
        cout << "Stu的析构"  << endl;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age << endl;
    }
};
int main()
{
    Stu s1("夜猫徐",18);
    s1.show();
    //显式调用类中拷贝构造,当类中并没有提供这个构造,编译器会自动给生成一个拷贝构造.
    //这个编译器会自动给生成的拷贝构造就是上面我们写的拷贝构造语法形式实现的。
    Stu s2(s1);
    s2.show();
    //隐式调用类中的拷贝构造。
    Stu s3 = s1;
    s3.show();
    return 0;
}

结果展示:
在这里插入图片描述
总结:

  1. 当有类中属性指针指向堆区,不可以使用默认拷贝和浅拷贝,应该使用深拷贝。不然在释放时会出现double free的错误。
  2. 浅拷贝升级为深拷贝的步骤 :1.开辟新空间 2.拷贝数据到新空间中

3.3.3 拷贝构造带来的问题即解决方式

以下面代码为负面例子:

#include <iostream>
using namespace std;
class Stu
{
private:
    string name;
    int age;
    int *p;
public:
    //有参构造
    Stu(string name, int age)
    {
        this->name = name;
        this->age = age;
        this->p=new int[20];//有类中属性指针指向堆区,升级为深拷贝
        cout << "Stu的构造"  << endl;
    }
    //编译器给我提供默认拷贝构造语法形式:这种简单的值的传递也称之为浅拷贝。浅拷贝对计算机性能消耗较低。
    Stu(const Stu& other)/*:name(other.name),age(other.age)*/
    {
        this->name=other.name;
        this->age=other.age;
        //指针成员也会只做简单的赋值,相当于两个对象的指针成员指向的是同一块空间,调用析构函数释放时,
        //就会出现 double free的问题不能简单的使用浅拷贝,要升级为深拷贝
        //深拷贝实现步骤
        //1.开辟新空间
        this->p=new int[20];
        //2.拷贝数据
        //memcpy(this->p, other.p,sizeof(int[20]));
        //memove针对小型嵌入设备的开发。
        memmove(this->p,other.p,sizeof(int[20]));//memmove有一种安全机制。
        cout << "Stu的拷贝构造" << endl;
    }
    ~Stu()
    {
        delete []p;
        cout << "Stu的析构"  << endl;
    }
    void show()
    {
        cout << "姓名:" << name << " 年龄:" << age << endl;
    }
    int getage()
    {
        return this->age;
    }
};
Stu compare_age(Stu stu1,Stu stu2)
{
    return stu1.getage()>stu2.getage()?stu1:stu2;
}
int main()
{
    Stu s1("夜猫徐",18);
    s1.show();
    Stu s4("zhangsan",20);
    cout << "-------------------" << endl;
    compare_age(s1,s4).show();
    return 0;
}

优化后代码:
其他不遍,加上引用。

Stu& compare_age(Stu& stu1,Stu& stu2)
{
    return stu1.getage()>stu2.getage()?stu1:stu2;
}

负面结果展示:
在这里插入图片描述
优化后结果展示:
在这里插入图片描述
总结:

  1. 使用引用来避免过多的没有意义的拷贝构造。
  2. 为了提代函数执行的高效性,推荐使用引用做为函数的参数,在适合的情况下(如果这个函数需要返回一个对象),那么也推荐使用引用。

3.4 C++中深拷贝和浅拷贝的区别

浅拷贝:
如果类中没有显性的定义拷贝构造函数,编译器会默认提供一个拷贝构造函数,这个默认提供的拷贝构造函数,只完成成员之间一对一的简单赋值,如果类中没有指针,使用这个默认的拷贝构造函数,是没有问题的。
深拷贝:
如果类中有指针成员,并且使用浅拷贝时,指针成员也会只做简单的赋值,相当于两个对象的指针成员指向的是同一块空间,调用析构函数释放时,就会出现 double free 的问题。所以需要在类中显性给定拷贝构造函数,并给新对象的指针成员重新分配空间,再将旧对象指针成员指向的空间里的数据复制一份过来。
图例:
在这里插入图片描述

四、匿名对象

没有名字但是有地址的

匿名对象产生的情况:

  1. 以值的方式给函数传参;
  2. 类型转换;
  3. 函数需要返回一个对象时;

使用匿名对象给对象赋值方式

类名 s=类名("小红",16);
/*在执行此代码时,利用构造函数生成了一个匿名类对象;然后将此匿名变成了s这个实例对象*/
类名();
/*在执行此代码时,利用无参构造函数生成了一个匿名类对象;执行完此行代码,
因为外部没有接此匿名对象的变量,此匿名又被析构了*/

匿名对象的生命周期
说明:

  1. 在执行类名( )时,生成了一个匿名对象,执行完后,此匿名对象就此消失。这就是匿名对象的生命周期。

  2. 在执行类名 s=类名(“小红”,16);时,首先生成了一个匿名对象,因为外部有s对象在等待被实例化,然后将此匿名对象变为了s对象,其生命周期就变成了s对象的生命周期。

总结:
如果生成的匿名对象在外部有对象等待被其实例化,此匿名对象的生命周期就变成了外部对象的生命周期;如果生成的匿名对象在外面没有对象等待被其实例化,此匿名对象将会生成之后,立马被析构。

代码示例:

#include <iostream>
using namespace std;

class Student{
private:
    string name;
    int age;
public:
    Student()
    {
        cout<<"无参构造函数"<<endl;
    }
    Student(string n, int a):name(n),age(a)
    {
        cout<<"有参构造函数"<<endl;
    }
    void show()
    {
        cout<<"姓名"<< name <<" 年龄"<< age << endl;
    }
    ~Student()
    {
        cout<<"析构函数"<<endl;
    }
};
void func(Student x)
{
    x.show();
}
int main(){
    Student();//匿名对象在外面没有对象等待被其实例化,此匿名对象将会生成之后,立马被析构。
    cout<<"-------------------"<<endl;
    Student s1("小明", 18);//有参构造
    s1.show();
    //使用匿名对象给s2赋值
    Student s2=Student("小红",16);
    cout<<"-------------------"<<endl;
    //匿名对象一般多用于类数组的初始化
    Student arr[5]={Student("a",10),Student("b",20)};
    cout<<"-------------------"<<endl;
    //或者用于给函数传参
    func(Student("张三",35));
    cout<<"-------------------"<<endl;
    return 0;
}

结果展示:
在这里插入图片描述

总结:
匿名对象一般多用于类数组的初始化,或者用于给函数传参的。

  • 40
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 61
    评论
评论 61
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜猫徐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值