C++类和对象(下)

server/2024/9/22 19:41:49/

文章目录

  • const成员函数
  • 取地址运算符重载
  • 初始化列表
  • 类型转换
  • static成员
  • 友元
  • 内部类
  • 匿名对象

const成员函数

概念:将const修饰的成员函数称之为const成员函数
定义:const修饰成员函数时,放到成员函数参数列表的后面

例:

class Date
{
public:Date(int year = 1990, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{const Date d1;d1.Print();return 0;
}

一般的,用普通对象来调用成员函数时,通过对象的地址传给隐含的this指针来访问成员变量,此时的对象d1是被const修饰的对象,当调用普通的Print函数时,是调不动的,因为这里出现了权限放大的现象,此时取d1的地址时,它的类型是const Date*,该const修饰指针所指向的内容,而Print函数的this指针的类型是Date* ,当把d1的地址传过去时,出现了权限放大的现象,所以就出现了const修饰this指针,它的类型就变成了const Date*

void Print()const

板图如下:
在这里插入图片描述
权限放大和缩小的总结:
const用于指针时,情况一,const位于星号之前,修饰指针所指向的内容,存在权限放大或缩小的情况;情况二,const位于星号之后,修饰指针本身,不存在权限放大和缩小的情况

const修饰成员函数,本质上修饰的是隐含的this指针,使该成员函数不能修改类中任何成员变量,普通的对象可以调用const成员函数,毕竟权限是可以缩小的,const对象也可以调用const函数,所以,不需要修改成员函数的成员函数都建议加上const

注:const在声明和定义时都要加上

取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,代码如下:

class Date
{
public:Date(int year = 1990, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//普通取地址运算符重载Date* operator&(const Date& d){return this;}//const取地址运算符重载const Date* operator&(const Date* d)const{return this;}
private:int _year;int _month;int _day;
};

一般情况下,取地址运算符重载编译器默认生成的就用了,不需要自己实现,除非有特殊要求,如:不想要别人取到地址,代码如下:

Date* operator&(const Date& d)
{return (Date*)nullptr;//或者给个假地址// return (Date*)0x11223fff;
}

初始化列表

定义:初始化列表是一种构造函数的初始化方式

初始化列表的格式:

初始化列表在构造函数的函数体外实现,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始化值或者表达式

例:

class Date
{
public:Date(int year,int month,int day)//初始化列表:_year(year),_month(month),_day(day){//函数体}
private:int _year;int _month;int _day;
};
int main()
{return 0;
}

初始化列表的特点:

  • 每个成员变量在初始化列表中只能出现一次,且每个成员变量都会走初始化列表,就算不在初始化列表中的成员变量也会走
class Time
{
public:Time(int hour = 10):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};
class Date
{
public:Date(int year = 1,int month = 1,int day = 1):_year(year),_month(month),_day(day)//_t没有在初始化列表中显示也会初始化列表{}
private:int _year;int _month;int _day;Time _t;
};
int main()
{Date d1;return 0;
}

在这里插入图片描述
可以看到_t 这个成员变量也走了初始化列表去调用默认构造函数,所以尽量使用初始化列表,因为那些不在初始化列表初始化的成员变量也会走初始化列表

为什么要有初始化列表呢?

大家可能会这么想,明明可以直接在函数体里使用赋值的方式进行初始化,为什么还需要初始化列表呢,因为有一些成员变量是必须要用初始化列表进行初始化的,我们来看下面这段代码(两个栈实现一个队列的部分代码):

class stack
{
public:stack(int capacity = 10){_a = (int*)malloc(sizeof(int) * capacity);if (_a == nullptr){perror("malloc fail!");exit(-1);}_capacity = capacity;_top = 0;}~stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;int _capacity;int _top;
};
class Myqueue
{
private:stack _popst;stack _pushst;
};
int main()
{Myqueue mq1;return 0;
}

在这里插入图片描述

对于自定义类型,编译器会自动调用它的默认构造函数,所以队列类不需要我们定义构造函数,但是,若将代码一改,将栈的构造函数的参数的缺省值去掉,会出现什么问题呢

在这里插入图片描述
上面图片中的报错说明,编译器找不到Myqueue的默认构造函数,无法对mq1进行初始化,要解决这样的问题,那就得显示定义Myqueue的默认构造,但是不能在默认构造的函数体中进行初始化,因为在函数体中调用不了自定义类型成员对应的构造函数,C++规定只能在初始化列表中才能调用自定义类型成员的构造函数,代码如下:

class stack
{
public:stack(int capacity){_a = (int*)malloc(sizeof(int) * capacity);if (_a == nullptr){perror("malloc fail!");exit(-1);}_capacity = capacity;_top = 0;}~stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;int _capacity;int _top;
};
class Myqueue
{
public:Myqueue(int n = 10):_popst(n),_pushst(n) // 会去调用栈的构造函数{}
private:stack _popst;stack _pushst;
};
int main()
{Myqueue mq1;return 0;
}

除了没有默认构造类类型成员变量还有引用成员变量const成员变量,这三种成员变量必须要用初始化列表进行初始化,若在函数体内初始化,编译器就会报错

在这里插入图片描述
根据初始化列表的特点,每个成员变量只能在初始化中出现一次,且每个成员变量都会走初始化列表,可以认为初始化列表是每个成员变量定义初始化的地方,根据上面三种成员变量在定义时就必须初始化且被const和引用修饰的成员变量一旦初始化就不可修改的特征,就必须使用初始化列表初始化,因为在函数体中成员变量可出现多次,编译器不知道哪一条语句才是成员变量要初始化的语句

  • C++支持在成员变量声明时给缺省值

看下面代码:

class Date
{
public:Date(int year = 2,int month = 2,int day = 2):_year(year),_month(month)// _day不在初始化列表中{}
private:int _year;int _month;int _day;
};
int main()
{Date d1;return 0;
}

在这里插入图片描述
_day 似乎没走初始化列表,编译器是这样的,对于自定义类型它会调用对应的默认构造,不在初始化列表中也会去调用,但若是调用的默认构造需要传参时,不在初始化列表中给值是不能成功调用的;对于内置类型不在初始化列表中也会走初始化列表,但上面的情况似乎没有被初始化,为了自定义类型能成功地调用默认构造和内置类型成功的在初始化列表中被初始化,C++补充了条语法,就是支持在成员变量声明时给缺省值,作用是给那些不在初始化列表的成员变量使用,若在初始化列表中显示了,就不会用到缺省值,代码如下:

class Time
{
public:Time(int hour):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};
class Date
{
public:Date(int year = 2,int month = 2,int day = 2):_year(year),_month(month){}
private:int _year;int _month;int _day = 1;Time _t = 5;
};
int main()
{Date d1;return 0;
}

在这里插入图片描述
有了缺省值,就可保证每个成员变量都会被初始化了

总结: 成员变量走初始化列表的逻辑

在这里插入图片描述
注意:初始化列表的初始化顺序与成员变量声明的顺序一致

class A
{
public:A(int a = 1):_a1(a),_a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a2;int _a1;
};
int main()
{A aa;aa.Print();return 0;
}

在这里插入图片描述
可看到,_a2 先被初始化,但这时的_a1 还是个随机值,所以出来的结果就是_a2 是随机值

类型转换

  • C++支持内置类型隐式转换成类类型,需要有相关内置类型为参数的构造函数即可
class A
{
public:A(int aa = 0) //内置类型为参数的构造函数:_a1(aa){}A(int a1,int a2):_a1(a1),_a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a1 = 0;int _a2 = 1;
};
int main()
{A aa1 = 1;aa1.Print();return 0;
}

在这里插入图片描述
可看到,内置类型的1 隐式类型转换给了类A中的_a1。这是怎么回事呢,本质上其实是1构造了一个A的临时对象,再用这个临时对象拷贝构造给aa1。但是编译器在这里做了个简单的优化,将构造一个临时对象再拷贝构造给aa1,优化成直接构造aa1,板图如下:
在这里插入图片描述

证明隐式类型转换时会产生临时对象:

const A& aa2 = 1;

这里引用了临时对象,临时对象具有常性,所以用const修饰。在这里编译器就不会优化了,若优化了这里的引用就变成了野引用了

  • 关键字explicit

若不想隐式类型转换发生就可在构造函数前面加上explicit

在这里插入图片描述

  • 多参数的隐式类型转换
A aa1 = {3,3}; // C++11才支持
  • 类型转换的意义
class A
{
public:A(int aa = 0):_a1(aa){}A(int a1,int a2):_a1(a1),_a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a1 = 0;int _a2 = 1;
};
class stack //用来存储A类型的栈
{
public:void Push(const A& aa){//...}
};
int main()
{// 不用隐式类型转换stack st1;A aa1;st.Push(aa1);// 直接隐式类型转换传参stack st2;st.Push(5);st.Push({6,6}); return 0;
}

隐式类型转换在传值传参写起来更方便等等

自定义类型转换为自定义类型可以转换吗?

借助有要转换的自定义类型的参数的构造函数即可

class A
{
public:A(int aa = 0):_a1(aa){}A(int a1, int a2):_a1(a1), _a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}int Get()const{return _a1 + _a2;}
private:int _a1 = 0;int _a2 = 1;
};
class B
{
public:B(const A& aa) // A类转换成B类,有A类类型的参数的构造函数:_b(aa.Get()){}void Print(){cout << _b << endl;}
private:int _b = 0;
};
int main()
{A aa1;B bb1 = aa1; // A隐式类型转换成了B// 同样构造aa1构造了一个B类的临时对象bb1.Print();return 0;
}

正是因为构造函数的参数中有A类型作为参数,才可成功隐式转换

总结:两个类型有一定的关联才能转换

static成员

  1. static成员变量

用stati修饰成员变量,称之为静态成员变量。静态成员变量一定要在类外进行初始化,它为所有对象共享,不属于某个类对象的专属,存放在静态区,通俗来讲就是,一个全局变量,被放到了类里面,受类的限制,变成了类的专属

  • 使用方法

举个例子:统计A类创建了多少个对象?

#include <iostream>
using namespace std;
class A
{
public:A(){++_count;}A(const A& aa){++_count;}
//private:static int _count; // 类里面声明
};
int A::_count = 0; // 类外定义
void func(A aa)
{}
int main()
{A aa1;A aa2 = aa1;func(aa1);// _count突破访问限定符时的访问方式有两种// static成员变量属于整个类和所有对象cout << A::_count << endl; cout << aa1._count << endl; //不代表_count属于aa1return 0;
}

在这里插入图片描述
一共创建了3个对象

  • 计算aa1和aa2的大小

在这里插入图片描述
计算的结果是1而不是4说明了静态成员变量不属于某个对象的专属,是1的原因是编译器为aa1和aa2开辟的一个空间

  1. static成员函数

在前面统计类创建了多少个对象的例子中,访问静态成员变量时,我们将其设为了公有,一般情况下,成员变量是不允许在类外直接访问的,所以想要不突破限定符的限制来访问静态成员变量,可以通过静态成员函数来进行访问

class A
{
public:A(){++_count;}A(const A& aa){++_count;}static int Get_count() // 静态成员函数{return _count;}
private:static int _count; // 类里面声明
};
int A::_count = 0; // 类外定义
void func(A aa)
{}
int main()
{A aa1;A aa2 = aa1;cout << A::Get_count() << endl;return 0;
}

在这里插入图片描述
像上面这样就可以访问静态成员变量了,但问题来了,为什么静态成员函数是这样调用,而不是下面这样的方式调用呢?

cout << aa1.Get_count() << endl;

这就与静态成员函数的特性有关了。我们知道,成员函数是有一个隐含的this指针的,选择用对象.成员函数的方式调用成员函数是为了传对象的地址给this指针,而上面的代码中调用静态成员函数时不需要用对象来进行调用,这就说明了,静态成员函数没有隐含的this指针,由于受到类的限制,所以调用时要用类来进行调用

  • 静态成员的其他特性
  • 静态成员函数只可访问静态成员变量,不能访问非静态成员变量,因为没有隐含的this指针,但非静态成员函数两者皆可访问
  • 静态成员也是类的成员,受类的限制
  • 静态成员变量在声明时不可以给缺省值,因为缺省值是给初始化列表准备的,静态成员变量不属于某个对象,不走构造函数进行初始化

友元

概念:友元是一种突破类访问限定符的方式,分为友元函数和友元类
格式:在函数声明或类声明前面加上friend,并把友元函数声明或友元类声明放到一个类里面

  1. 友元函数

特性:

  • 外部友元可以访问类的私有和保护成员,友元函数是声明不是成员函数
class A
{friend int func(A& aa); //友元函数声明
public:A(int a1 = 1,int a2 = 2):_a1(a1),_a2(a2){}
private:int _a1;int _a2;
};
int func(A& aa)
{return aa._a1 + aa._a2; // 可访问私有成员变量
}
int main()
{A aa1;cout << func(aa1) << endl;return 0;
}
  • 友元函数可以在类里的任何地方定义,不受类的访问限定符限制
  • 一个函数可以是多个类的友元函数
  1. 友元类

友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员

class A
{friend class B; // 友元类声明
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:// 友元类中的成员函数void func1(const A& aa){//直接访问A类中的私有和保护成员cout << aa._a1 << " " << _b1 << endl;}void func2(const A& aa){cout << aa._a2 << " " << _b2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};
int main()
{A aa1;B bb1;bb1.func1(aa1);bb1.func2(aa1);return 0;
}

注:友元是单向且不能传递

内部类

概念:把一个类定义在另一个类里面

例:

class A
{
private:int _a = 1;static int _i;
public:class B // B默认是A的友元类{public:void func(const A& aa){// 可访问A的私有和保护成员cout << _i << endl;cout << aa._a << endl;cout << _b << endl;}private:int _b = 1;};
};
int A::_i = 1;
int main()
{A::B b; // 因为B受到A类的限制,所以要这样访问A aa1;b.func(aa1);return 0;
}

在这里插入图片描述
在内部类的条件下,探索两个类之间的关系

可通过计算外部类的大小来确定,如下:

在这里插入图片描述
从计算结果看出,A类的大小是4,说明了A中只存储了_a 这个变量,而_i 是静态成员变量,存储在静态区中,所以可以得到,外部类和内部类不是包含关系,而是单独的两个类,只是内部类受到了外部类的类域和访问限定符的限制而已

注:内部类默认是外部类的友元类

匿名对象

概念:用类型(实参)定义出来的对象叫作匿名对象,相比我们之前定义的类型+对象名定义出来的叫作有名对象

例:

class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}
private:int _a;
};
int main()
{//有名对象A aa1(1);A aa2; // 有名对象不传参时,不需要加括号// 匿名对象A(1);A(); // 匿名对象不传参时,必须加括号return 0;
}

特点: 匿名对象的声明周期只在当前一行,过来定义那一行声明周期结束

class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
int main()
{A aa1(1);A aa2;A(1);A(2);cout << "###################" << endl;return 0;
}

在这里插入图片描述
看上面的图片,我们知道有名对象的析构函数是在程序运行完之后才调用的,但是我们可以看到匿名对象创建完之后就被析构了,说明匿名对象的声明周期只在当前一行

匿名对象可用于作为函数的缺省值,如下:

void func(A aa = A(10))
{// ....
}

匿名对象可以用引用&,如下:

const A& ra = A();

匿名对象与临时对象相似,需要const修饰
注意:此时的匿名对象的周期得到了延长,若不得到延长,一旦结束了匿名对象的生命周期,就会出现野引用的现象


http://www.ppmy.cn/server/120421.html

相关文章

Selenium等待机制:理解并应用显式等待与隐式等待,解决页面加载慢的问题

目录 引言 等待机制的重要性 显式等待&#xff08;Explicit Wait&#xff09; 原理 应用方式 代码示例 优点与缺点 隐式等待&#xff08;Implicit Wait&#xff09; 原理 应用方式 代码示例 优点与缺点 解决页面加载慢的问题 1. 合理设置等待时间 2. 优先使用显…

【JavaEE初阶】多线程6(线程池\定时器)

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 实例3:线程池 参数解释 核心线程数, 最大线程数 允许空闲的最大时间 ,时间单位 任务队列(阻塞队列) 线程工厂>工厂设计模式 拒绝策略 使用举例 模拟实现一个线…

如何在Linux Centos7系统中挂载群晖共享文件夹

前景&#xff1a;企业信息化各种系统需要上传很多的图片或者是文件&#xff0c;文件如何在群晖中显示&#xff0c;当文件或者图片上传到linux指定文件夹内&#xff0c;而文件夹又与群晖共享文件夹进行挂载&#xff0c;就能保证上传的文件或者图片出现在群晖并在群晖里进行管理。…

Mac 上,终端如何开启 proxy

文章目录 为什么要这么做前提步骤查看 port查看代理的port配置 bash测试 为什么要这么做 mac 上的终端比较孤僻吧&#xff0c;虽然开了&#xff0c;但是终端并不走&#x1fa9c;…产生的现象就是&#xff0c;浏览器可以访问&#x1f30d;&#xff0c;但是终端不可以访问&#…

深入理解与避免Java 死锁

在 Java 编程中&#xff0c;死锁是一个让人头疼但又至关重要的问题。理解死锁的产生条件以及如何避免死锁&#xff0c;对于编写高效、稳定的多线程程序至关重要。本文将深入探讨 Java 死锁的四个必要条件&#xff0c;并通过具体的例子和解决方案帮助读者更好地理解和避免死锁。…

集群聊天服务器项目【C++】(六)MySql数据库

前面已经介绍了网络模块和业务模块&#xff0c;本章介绍数据模块&#xff0c;同样保持模块解耦的特性&#xff0c;即业务模块不能出现数据模块内容&#xff0c;如出现SQL语句&#xff0c;接下来看看怎么实现的。 1.环境安装 第一章已经介绍了MySql安装&#xff0c;但注意需要…

【protobuf】ProtoBuf的学习与使用⸺C++

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 前言&#xff1a;之前我们学习了Linux与windows的protobuf安装&#xff0c;知道protobuf是做序列化操作的应用&#xff0c;今天我们来学习一下protobuf。 目录 ⼀、初识ProtoBuf 步骤1&#xff1a;创建.proto文件 步…

1. ZYNQ 2. MPSOC 3. FPGA 4. Vitis 5. 项目

### 1. 建立Vitis SDK自带的Hello World工程 首先&#xff0c;我们需要在Vitis SDK中创建一个基本的Hello World工程。这是学习FPGA开发和ZYNQ MPSOC平台的重要第一步。Hello World工程的主要目的是验证开发环境的正确性以及熟悉基本的编程流程。 #### 步骤&#xff1a; - 打开…