【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)

简介: 本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。

前言

       之前我们学习了类中的一些默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载。今天,我们接着学习剩下的取地址运算符重载以及其他关于类和对象的知识


一、取地址运算符重载

       取地址运算符重载分为两种:普通对象的取地址重载const对象取地址重载。为了说明这两种取地址重载的区别,我们首先引入一个概念:const修饰成员函数


1. const修饰成员函数

       在c++中,成员函数可以被const修饰,修饰时要将const写在成员函数参数列表的后面。例如:

void fun() const
{
 
}

const修饰成员函数的本质是修饰this指针指向的内容,它的作用是防止该函数内部对成员变量的值进行修改。



对于一个普通成员函数,const对象是无法调用的,因为const对象的成员变量不允许被修改;而当成员函数被const修饰时,就确保了函数内部不会修改成员变量的值,const对象就可以调用该函数。


2. 取地址运算符重载

      普通对象的取地址重载用于返回普通对象的地址;而const对象的取地址重载用于返回const对象的地址。两种重载函数的区别是:前者没有被const修饰,后者被const修饰。这就使得两个函数构成了重载,便于不同的对象调用。我们简单实现一下这两个函数:

class MyClass
{
public:
    //构造函数
    MyClass(int a = 10, int b = 20)
    {
        _a = a;
        _b = b;
    }
 
    //取地址重载
    MyClass* operator&()
    {
        return this;
    }
    const MyClass* operator&() const//为保证类型匹配,返回值也要用const修饰
    {
        return this;
    }
private:
    int _a;
    int _b;
};

一般情况下,编译器自动生成的取地址重载函数我们就可以直接使用,不需要显示实现了。当我们不希望使用者能够获取到对象的地址时,可以显示实现取地址重载,并将空指针或者野指针作为返回值。


二、深究构造函数

       之前我们已经学习了构造函数的特点、使用规则等知识,不过构造函数的知识还不止这些,接下来我们对之前构造函数的内容进行一些补充。


       之前我们在实现构造函数时,都是在函数体内部对成员变量赋初值,实际上,对成员变量进行初始化的方式还有一种:初始化列表。它位于构造函数的参数列表之后,函数体大括号之前。它的使用方式是以冒号开始,将需要被初始化的成员以逗号分隔,成员之后写一个放在括号当中的值或者表达式,用于初始化成员。例如:

//构造函数
MyClass(int a = 10, int b = 20)
    :_a(a)//将a的值给成员变量_a
    , _b(b)//将b的值给成员变量_b
{
    //...
}

需要注意的是:


1. 每一个成员变量在初始化列表中只能出现一次。


2. 初始化列表初始化的顺序与成员在类中的声明顺序一致,而与列表中的成员顺序无关。


3. 以下三种变量必须在初始化列表中进行初始化,否则会编译报错:引用类型的成员变量、const成员变量、不存在默认构造的类类型成员变量。


这里的 “ 必须在初始化列表中进行初始化 ” 并不是指我们一定要将该变量显示写在初始化列表中,我们也可以使用如下方式:

class MyClass
{
public:
    //构造函数
    MyClass(int a = 10, int b = 20)
        :_a(a)
        , _b(b)
    {
        //...
    }
private:
    int _a;
    int _b;
    const int _c = 1;
};

可以看到,对于const成员“_c”,我们并没有显示在初始化列表中对其进行初始化,而是在其声明时为其赋了一个缺省值(初值)。这是c++11规定的语法,该初值是给初始化列表的,当初始化列表当中没有显示对一个成员进行初始化时,如果声明时有缺省值,则会用这个值进行初始化(本质也是通过初始化列表初始化,只不过并没有显示写出),所以程序并不会发生报错。当然,对于普通成员,我们也可以在声明时赋缺省值,但是相比显示写在初始化列表当中,会有一些效率的损耗。


注:对类类型的成员变量通过初始化列表进行初始化时,本质也是在调用它的构造函数


       如果我们既没有显示地在初始化列表对成员进行初始化,也没有在声明时赋缺省值,那么对于内置类型的成员,当对象被创建时编译器一般不会对其初始化;对于自定义类型的成员,对象被创建时就会调用它的默认构造函数,如果没有默认构造函数,就会发生报错。


接下来,我们总结一下成员变量通过初始化列表进行初始化的逻辑:



三、类型转换

       首先来看一段代码:

class MyClass
{
public:
    //构造函数
    MyClass(int a = 10)
        :_a(a)
    {
 
    }
    void Print() const 
    {
        cout << _a << endl;
    }
private:
    int _a;
};
 
int main()
{
    MyClass m = 1;
    m.Print();
    return 0;
}

上述程序中,我们创建了一个MyClass类对象m,并且将其初始化为1。我们都知道,它的本质是在调用构造函数,不过它的运行过程并不是这么简单。在 MyClass m = 1 语句中,等号右边的 “ 1 ”是整形,而“ m ”是MyClass类型,这个过程中就需要发生类型转换。程序首先会调用构造函数,将“ 1 ”构造为MyClass类型的一个临时对象,然后将该临时对象拷贝构造给m。对于这种调用构造函数+调用拷贝构造的情况,有些编译器会将其优化为直接调用构造函数,所以我们可能无法感受到类型转换的过程,但它的确是存在的。当我们在构造函数之前加上关键字“ explicit ”之后,就无法调用该构造函数进行隐式类型转换。当然,如果有合适的构造函数,类与类之间也可以发生类型转换。


       对于有多个参数的情况,也可以进行类型转换:

class MyClass
{
public:
    //构造函数
    MyClass(int a = 10, int c = 20)
        :_a(a)
        ,_c(c)
    {
 
    }
    void Print() const 
    {
        cout << _a << endl;
        cout << _c << endl;
    }
private:
    int _a;
    char _c;
};
 
int main()
{
    MyClass m = { 1,'w' };//大括号赋值的写法C++11以后才支持
    m.Print();
    return 0;
}

四、static修饰成员

       在C++当中,static可以修饰成员变量成员函数,它们在面向对象编程中有着很重要的作用。


1. static修饰成员变量

       用static修饰的成员变量叫做静态成员变量。静态成员变量要在类中进行声明,并且初始化必须要在类外,而不是类中(因为在类中给的初值是给初始化列表的,而静态成员变量不走初始化列表)。例如:

class MyClass
{
public:
    //...
private:
    static int _m;//类里面声明
};
 
int MyClass::_m = 0;//类外进行初始化

并且在多个文件的程序中,如果类是在头文件中定义的,则静态成员变量必须在其他cpp文件中初始化,否则容易出现重定义问题。


特殊情况:对于有const修饰的整形静态成员变量,可以在类中同时进行声明和初始化。


注意:静态成员变量存储在静态区中,不属于任何一个对象,而是被所有对象所共享的,使用对象或者类的作用域限定符就可以访问到静态成员变量。当然,既然是成员变量,也会受到 public / private / protected 的限制。接下来,我们尝试通过不同方式访问静态成员变量:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //...
    static int _m;
};
 
int MyClass::_m = 5;
 
int main()
{
    cout << MyClass::_m << endl;//使用作用域限定符访问
 
    MyClass a;
    cout << a._m << endl;//使用对象访问
    return 0;
}

运行结果:


由于 _m 是公有成员,所以我们直接访问到了该变量。当静态成员变量是私有成员时,该如何访问呢?这就需要静态成员函数了。


2. static修饰成员函数

       用static修饰的成员函数称之为静态成员函数,静态成员函数与普通成员函数的显著区别是:它不存在this指针


       由于静态成员函数不存在this指针,所以它也就无法访问到普通成员变量,只能访问静态成员变量。当然,如果一个成员函数是非静态的,它也可以访问静态成员变量。接下来我们用静态成员函数来访问私有的静态成员变量:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    static int func()
    {
        return _m;
    }
private:
    static int _m;
};
 
int MyClass::_m = 5;
 
int main()
{
    cout << MyClass::func() << endl;//通过作用域限定符调用函数
 
    MyClass a;
    cout << a.func() << endl;//通过对象调用函数
    return 0;
}

运行结果:



3. 小练习

       接下来我们运用static修饰成员的知识,尝试做一个小练习:计算1+2+3+...+n的值,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。


       思路分析:要计算1+2+3+...+n的值,习惯的思路是循环累加或者使用等差数列求和公式,但是由于禁止使用乘除法和循环语句,这两种方法就行不通了。那么递归能否解决呢?由于递归一定要有限制条件,而if...else语句也被禁用了,所以递归也是不行的。那该怎么办呢?我们都知道,静态成员变量为所有对象所公有,那么我们就可以定义一个静态成员变量x,每当一个对象被创建出时,x的值就+1。这样,我们创建出n个对象,并将每一次得到的x值累加到另一个变量中,就可以算出最终结果了。


       话不多说,我们尝试实现:

class MyClass
{
public:
    //在构造函数中操作静态变量,使得对象被创建时就会累加
    MyClass()
    {
        x++;
        sum += x;
    }
 
    //获取sum的值
    static int GetNum()
    {
        return sum;
    }
private:
    static int x;
    static int sum;
};
 
//初始化
int MyClass::x = 0;
int MyClass::sum = 0;
 
//求和函数
int Sum(int n)
{
    //用new关键字创建n个对象
    MyClass* arr = new MyClass[n];
 
    //返回累加后的值
    return MyClass::GetNum();
}

我们测试一下程序的正确性:

int main()
{
    int n = 0;
    cout << "请输入n值";
    cin >> n;
    cout << "结果为:" << Sum(n) << endl;
    return 0;
}

运行结果:


五、友元

       当类中的成员被设置为私有,外部无法访问到时,友元就可以突破这种封装,使得外部可以访问这些私有成员。友元可以分为友元函数和友元类,我们需要使用友元时,在函数或类的声明之前加上关键字 friend ,并将其放在另一个类(宿主类)当中。此时该函数或类就成为了宿主类的友元。


       友元函数和友元类的特点如下:


1. 友元函数只是一种声明,并不是说一个函数成为了一个类的友元,他就是该类的成员函数了。


2. 友元函数可以在类的任意地方声明,并不受public等限定符的限制。


3. 一个函数可以成为多个类的友元。


4. 友元类中的成员函数都可以访问宿主类的成员,不受限定符限制。


5. 友元类的关系是单向且不可传递的,比如:A是B的友元,但不意味着B是A的友元;A是B的友元,B是C的友元,并不意味着A是C的友元。


接下来我们尝试使用一下友元:

#include <iostream>
using namespace std;
 
class A
{
public:
    friend void fun1();//函数fun1称为类A的友元
    friend class B;//类B成为类A的友元
private:
    int _a = 1;
protected:
    int _b = 2;
};
 
void fun1()
{
    A a;
    cout << a._a << endl;
    cout << a._b << endl;
}
 
class B
{
public:
    void fun2()
    {
        A a;
        cout << a._a << endl;
        cout << a._b << endl;
    }
};
 
int main()
{
    fun1();
    cout << endl;
    B b;
    b.fun2();
    return 0;
}

运行结果:


不难看出,将函数或类声明为友元后,就可以访问宿主类的私有或保护成员了。友元有时虽然提供了便利,但是它明显是破坏了类的封装性,不符合“高类聚,低耦合”的设计原则,所以实际开发中不宜多用。


六、内部类

       如果一个类A定义在另一个类B当中,那么类A就成为了类B的内部类。内部类与全局定义的类相比,它受到外部类的类域和访问限定符限制,并且默认是外部类的友元类。这里要注意:内部类是一个类定义在另一个类当中,而不是将对象作为一个类的成员,不要将两者混淆


       内部类的本质也是一种封装的体现,当我们需要让一个类B仅供类A使用,那么就可以考虑让B成为A的内部类。


       接下来我们定义一个内部类:

#include <iostream>
using namespace std;
 
class A
{
public:
    class B//此时B是A的内部类
    {
    public:
        void Print(const A& a)
        {
            cout << a._m << endl;
            cout << a._n << endl;
        }
    };
private:
    int _m = 3;
    int _n = 5;
};
 
int main()
{
    A a;
 
    A::B b;//受到类域限制,声明时要限定类域
    b.Print(a);
    return 0;
}

运行结果:



七、匿名对象

       顾名思义,匿名对象就是没有实际名字的对象,它的定义方法是:

MyClass(10);//构造函数传参
MyClass();//不传参

注意:匿名对象的生命周期只有当前一行,当程序运行到下一行时,该对象就被销毁。我们来验证一下:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    MyClass(int a = 1)
        :_a(a)
    {
        cout << "调用构造函数" << endl;
    }
    ~MyClass()
    {
        cout << "调用析构函数" << endl;
    }
private:
    int _a;
};
 
int main()
{
    MyClass();
    cout << "hehe" << endl;
    return 0;
}

运行结果:


可以看到,程序在打印hehe之前,就已经调用了析构函数,意味着匿名对象已经被销毁。


       当我们需要创建一个临时对象,并且只使用一次时,就可以考虑创建匿名对象。相比普通对象,它能够很大限度地简化代码。


      特别注意:当匿名对象被const引用接收时,它的生命周期会延长至该引用的作用域结束。


总结


       今天我们学习了类和对象相关的新概念和知识,例如:取地址重载、static修饰成员、友元、内部类等,它们对于我们深入学习并理解c++的后续内容,以及实现对象的相关功能有很大帮助。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
10天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
14天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
5天前
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
|
10天前
|
人工智能 运维 双11
2024阿里云双十一云资源购买指南(纯客观,无广)
2024年双十一,阿里云推出多项重磅优惠,特别针对新迁入云的企业和初创公司提供丰厚补贴。其中,36元一年的轻量应用服务器、1.95元/小时的16核60GB A10卡以及1元购域名等产品尤为值得关注。这些产品不仅价格亲民,还提供了丰富的功能和服务,非常适合个人开发者、学生及中小企业快速上手和部署应用。
|
5天前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
|
21天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3945 4
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
10天前
|
算法 安全 网络安全
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
2024阿里云11.11金秋云创季活动火热进行中,活动月期间(2024年11月01日至11月30日)通过折扣、叠加优惠券等多种方式,阿里云WoSign SSL证书实现优惠价格新低,DV SSL证书220元/年起,助力中小企业轻松实现HTTPS加密,保障数据传输安全。
530 3
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
|
9天前
|
数据采集 人工智能 API
Qwen2.5-Coder深夜开源炸场,Prompt编程的时代来了!
通义千问团队开源「强大」、「多样」、「实用」的 Qwen2.5-Coder 全系列,致力于持续推动 Open Code LLMs 的发展。
|
16天前
|
安全 数据建模 网络安全
2024阿里云双11,WoSign SSL证书优惠券使用攻略
2024阿里云“11.11金秋云创季”活动主会场,阿里云用户通过完成个人或企业实名认证,可以领取不同额度的满减优惠券,叠加折扣优惠。用户购买WoSign SSL证书,如何叠加才能更加优惠呢?
995 3
|
14天前
|
机器学习/深度学习 存储 人工智能
白话文讲解大模型| Attention is all you need
本文档旨在详细阐述当前主流的大模型技术架构如Transformer架构。我们将从技术概述、架构介绍到具体模型实现等多个角度进行讲解。通过本文档,我们期望为读者提供一个全面的理解,帮助大家掌握大模型的工作原理,增强与客户沟通的技术基础。本文档适合对大模型感兴趣的人员阅读。
447 18
白话文讲解大模型| Attention is all you need