类和对象(下)

news/2024/11/23 1:03:58/

类和对象(下)

  • 再谈构造函数
    • 构造函数体赋值
    • 初始化列表
    • explicit关键字
  • static成员
    • 静态成员的特性
  • 友元
    • 友元函数
    • 友元类
    • 成员函数做友元
  • 内部类
  • 匿名对象
  • 编译器的一些优化

再谈构造函数

构造函数体赋值

在创建对象的时候编译器会调用构造函数给对象中的成员变量一个合适的初始值:

class Date
{
public:
Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
private:
int _year;
int _month;
int _day;
};

虽然在调用构造函数过后,对象中的每个成员都有了合适的值,但是不能将其称为对对象中的成员变量的初始化,构造函数中的语句最多叫赋初值(赋值),因为初始化只能进行一次,赋值可以进行很多次!

初始化列表

我现在创建了一个对象:Date d1(2022,2,3);
那么在我实例化d1对象的时候,_year、_month、_day的空间也随之开好了,现在我们要的对这块空间进行初始化啊!那么到底在哪里完成对这些成员变量的初始化呢?
祖师爷最后最后规定在执行构造函数之前先进行初始化,于是他将初始化这个动作放在了构造函数的后面,语言上不变描述那个位置,我们用图片表示:
在这里插入图片描述
这块进行初始化操作的区域,被称为初始化列表!
对于这块区域的写法,祖师爷也是有规定的:初始化列表以":"开头,分隔每个成员变量后面跟一个放在()里面的初始值或表达式
具体写法如下:
在这里插入图片描述
当然对于自定义类型子类,我们也能让其调用指定的属于它的构造函数来对其进行初始化:
在这里插入图片描述

那么为啥会有初始化列表勒?
主要是因为我们可能会在类的成员变量中定义一些const修饰的成员变量或引用成员变量:
在这里插入图片描述
我们在实例化出对象过后,其成员变量的空间也随之开辟完毕,对于普通成员变量,我们可以不用初始化,但是对于const修饰的成员变量与引用成员变量必须初始化,因为这些变量只有一次初始化的机会;
未对const修饰的成员变量及引用成员变量进行初始化,编译器出现了报错:
在这里插入图片描述

因此为了解决某些成员变量必须初始化的问题,C++就提出了初始化列表的操作,在初始化列表区间完成成员变量的初始化;后来随着C++的不断发展,在C++11中允许在成员变量声明的时候给缺省值,这样的作法也是可以的,但是在以前的C++标准中只有初始化列表才能解决这个问题;
在这里插入图片描述

注意:
1、每个成员变量只能在初始化列表出现一次;
2、const修饰的成员便量、引用成员变量、没有默认构造函数的自定义类型,必须进行初始化,那么为什么没有默认构造函数的自定义类型必须初始化呢?因为如果我们没有显示初始化自定义类型的话,编译器就会调用默认构造函数来完成初始化,但是自定义类型又没得,编译器就会报错;
3、尽量使用初始化列表,因为不管你是否使用初始化列表,编译器都会先去初始化列表看一看,然后再去成员变量的声明处看看是否有缺省值,对于未在初始化列表初始化的成员变量就使用对应函数声明出给的缺省值进行初始化!最后才去执行构造函数体!
4、在初始化列表,编译器会按按照成员变量的声明顺序来初始化,与成员变量在初始化列表的位置顺序无关;
我们通过一个例子来观察这种现象:
在这里插入图片描述
允许结果截图:
在这里插入图片描述
那么为什么会出现这种情况?
根据上面的理论,aa对象调用了A(int a)构造函数,但是编译器不会马上去执行函数体,而是先去初始化列表看一看,有没有初始化的操作,编译器发现的确有,于是按照成员变量的声明顺序,先对_a2初始化,_a2是用_a1初始化的,但此时_a1还没完成初始化,里面是随机值,因此_a2也就被初始化成随机值,随后编译按照顺序再完成了_a1的初始化,_a1是用a初始化的,a是1;因此最后我们看到的结果是_a1输出1,_a2输出随机值;

explicit关键字

对于只有一个参数的或者第一个参数没有给缺省值其余参数都给缺省值的构造函数具有类型转换的作用;
我们来看看实例,来详细介绍构造函数的类型转换作用:
在这里插入图片描述
为什么10(int)可以用来给d1(Date)初始化?
主要是因为int类型向Date类型赋值进行了隐式类型转换10并不是直接赋值给d1的,具体作法就是:
在这里插入图片描述

那么编译器是如何通过10来构造Date类型的临时变量的呢?
我的理解是:编译器首先实例化好一个Date类型的临时变量,然后将10作为Date类型的构造函数的参数来构建临时变量,这样就完成了从int类型向Date类型的转换,是谁完成这次转换工作的?是构造函数,因此才说构造函数具有类型转换的作用!

但是,我们会发现这是不是上面的转换是不是很麻烦又要创建临时变量、又要完成拷贝构造;况且我们都能用10来构造临时变量,那么我能不能直接用10来构建d1?当然是可以的,在一些比较新的编译器基本上都采用了这种优化!减少了不必要的中间过程,提高了程序效率,何乐而不为?这是编译器最喜欢干的!当然比较老的编译器(比如:VC++6.0)就没有采用上面的优化,还是一步一步走的!(博主用的是VS2022)
为此对与Date d1=10;这条语句我们会看到d1会去调用Date(int day)这个构造函数,而不会去调用
拷贝构造;我们通过运行结果来验证:
在这里插入图片描述
我们会发现的确实这样;当然如果我们想要取消构造函数的的类型转换功能(也就是取消利用其他类型来构建Date对象)我们可以在构建值类型与构造函数类型相匹配的构造函数前面加上explicit关键字来修饰,以此来取消该构造函数的类型转换功能;比如:我们现在不想让利用int类型转换为Date类型,我们就可以在Date(int day);构造函数前面加上explicit来修饰,就相当于取消了int类型转换为Date类型的“桥梁”,如果我们再次使用int类型转换为Date类型编译器就会报错:
在这里插入图片描述
我们删掉explicit就又可以从int转换为Date类型了;
当然我们上面说的是 “对于只有一个参数的或者第一个参数没有给缺省值其余参数都给缺省值的构造函数具有类型转换的作用” ,这是C98的标准,在C11中具有多个参数的构造函数也具有类型转换的作用,也就是说我们可以用{10,20,34}这种方式来构建Date类,前提是存在这样的Date(int,int,int)这样的构造函数,为此我们可以来实验一把:
在这里插入图片描述
运行截图:
在这里插入图片描述
当我们对Date(int year, int month, int day=1)构造函数加上explicit关键字修饰时编译器无法完成从{12,13}类型向Date类型及{1,1,2}类型向Date类型的转换,编译器会报错;
在这里插入图片描述
你删掉explicit程序又可以跑起来了!

static成员

static成员:
static修饰的成员变量被称为静态成员变量
static修饰的成员函数被称为静态成员函数,静态成员函数没有this指针,也就是说对于静态成员函数来说不需要知道是哪个具体的对象在调用它!,;
静态成员(成员变量和成员函数)由所有对象共享!不属于某个具体的对象,对于静态成员来说不需要知道是那个具体的对象在调用它们,因此对于静态成员来说,我们既可以通过对象调用,也可以通过类域调用!;
同时静态成员变量不能在初始化列表进行初始化,也不能在声明处给缺省值,这些地方都是针对非静态成员变量的!静态成员变量只能在类外初始化,初始化的时候需要指定是那个类域下的静态成员变量;

面试题: 实现一个类,并统计利用该类实例化了多少个对象!

分析: 我们知道每实例化一个对象必调用构造函数,那么我闷就可以从构造函数内部入手,我们可以定义一个全局变量,没实例化一个对象就在构造函数内部++这个全局变量;

代码如下:

#include<iostream>
using namespace std;
int g_count = 0;
class Date
{
public :Date(){g_count++;cout << "Date()" << endl;}Date(int year, int month, int day){g_count++;cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d){g_count++;cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
void Func(Date d)
{Date d1;Date d2(2022, 2, 2);Date d3(d2);
}
int main()
{Date d0;Func(d0);cout << g_count << endl;return 0;
}

运行结果:
在这里插入图片描述
的确实创建了5个对象;
这样的代码确实能够得到我们想要的结果,但是如果有时候我们不小心,对g_count手动++了呢?
在这里插入图片描述
g_count作为全局变量,太自由了!任意位置都能访问!容易造成数据的滥用;
为此我们需要限定一下g_count的自由,但同时生命周期要是整个程序的!
为此我们可以利用static修饰的成员变量来完成,最后返回静态变量的值就好了;
在这里插入图片描述

运行结果:
在这里插入图片描述
的确也是符合预期的,但是现在有个问题,就是我们想调用GetCount()函数的话,还要专门创建一个对象,这是不是就在我们的预期上多了一个,就比如原先程序上只有5个对象,然后让你统计这5个对象的个数,为了统计出这5个对象,我们还得创建一个对象用来专门调用GetCount函数,因为GetCount函数是动态成员函数,需要传递this指针,必须通过对象调用;所以我们统计到的对象会比原来的多一个,既然都知道了多一个我们在输出的时候直接减去不就好了嘛!这是在我们知道的情况下,假设我们不知道真实情况呢?所以我们的代码还需要改进,有没有不需要通过对象调用的函数呢?
当然有,静态成员函数嘛!静态成员函数是不需要传递this指针的,也就是说静态成员函数不需要知道是谁调用了它,但是它还是一个成员函数,属于该类域,我们就可以通过类域去调用它!因此我们可以将GetCount函数变为静态函数:
在这里插入图片描述
运行结果:
在这里插入图片描述

静态成员的特性

1、静态成员不属于某个具体对象,为所有对象共享,存放在静态区;
2、静态成员既可以通过对象访问,也可以直接用类域访问;
3、静态成员变量必须在类外定义,在类内的只是声明,并没有在内存中开辟实际空间!只有在类外定义过后才会有实际空间,才能被使用;
4、静态成员函数没有隐藏的this指针,因此在静态成员函数内部不能访问非静态成员变量;
5、静态成员也是类的成员,也受访问限定符的限制!

提问:
1、静态成员函数可以调用非静态成员函数吗?
答:可以!只不过我们需要创建对象,然后通过对象来访问非静态成员,无法直接访问非静态成员函数,因为静态成员函数内部没有this指针,调用非静态成员函数是需要传递this指针,如果在静态成员函数内部直接访问非静态成员函数的话,无法完成this指针的调用,编译器会报错;
2、非静态成员函数可以调用静态成员函数吗?
答:可以!调用静态成员函数不需要this指针,可以完成传参要求,自然也就可以调用静态成员函数;同时非静态成员函数有this指针,也可以调用其他非静态成员函数!

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。
友元分为:友元函数和友元类

友元函数

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的
输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作
数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

class Date
{
public:
Date(int year, int month, int day): _year(year), _month(month), _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{_cout << _year << "-" << _month << "-" << _day << endl;return _cout;
}
private:
int _year;
int _month;
int _day;
};

友元函数可以直接访问类的私有成员,它是定义在类外部普通函数,不属于任何类,但需要在类的内部声明声明时需要加friend关键字

class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}

总结:
1、友元函数可以访问类的私有、保护、公有成员,但不是类的成员函数;
2、可以在类内任何地方声明,声明的时候需要加上friend关键字修饰;
3、友元函数不能加const,因为对于成员函数来说加上const并不是用来修饰函数本身的,而是修饰*this的,友元函数是类外的一个函数不属于成员函数,自然不可能拥有this指针!this指针都没有const自然无法修饰;
4、一个函数可以是多个类的友元函数;
5、友元函数与普通函数调用方式一样!

友元类

class Time
{friend class Date;  // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}void SetTimeOfDate(int hour, int minute, int second){// 直接访问时间类私有的成员变量_t._hour = hour;_t._minute = minute;_t._second = second;}
private:int _year;int _month;int _day;Time _t;
};

总结:

1、友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3、友元关系不能继承,在继承位置再给大家详细介绍。

成员函数做友元

欢迎跳转至我的另一篇博客:C++核心编程(三)

内部类

概念: 如果把一个类定义在另一个类的内部,这个类就叫做内部类;
内部类是一个独立的类,不属于外部类,不能通过外部类的对象访问内部类的成员,外部类对于内部类来说没有任何访问权限;但是反过来,在内部类中,可以无限制的访问外部类的所有成员,简而言之就是内部类是外部类天然的友元类
特性:
1、内部类只是在外部类的类域中,在利用外部类实例化出来的对象不包含内部类实例化出来的空间;
这一点我们可以通过sizeof来验证:
在这里插入图片描述
利用A类实例化出来的对象大小是4字节,而非8字节!如果是8字节的话,那么就说明A实例化出来的对象的空间包括了B类的对象的空间!
2、我们无法直接使用内部类来实例化对象,因为内部类的定义是在外部类的作用域下面的,不在全局域;如果我们直接使用内部类实例化对象的话,编译器在全局域找不到内部类的定义,会直接报错!
因此我们需要显示的告诉编译器内部类的定义所在的作用域,让其去外部类的作用域下寻找内部类的作用域;
在这里插入图片描述
3、内部类的访问也是受外部类的访问限定符的限制,如果我们将内部类定义在外部类的protected、private区域,我们在外部类的外部是无法通过外部类::内部类 对象名这样的方式实例化内部类,编译器会告诉我们不可访问!
在这里插入图片描述
4、在内部类内部可以直接访问外部类的静态成员,可以不需要通过类名::对象.的方式调用;
在这里插入图片描述

匿名对象

顾名思义就是没有名字的对象;

class A
{
public:A():_a(0){cout << "A()" << endl;}A(int a) :_a(a){cout << "A(int a)" << endl;}A(const A& a):_a(a._a){cout << "A(const A& a)" << endl;}~A(){cout << "~A()" << endl;}void test(){cout << "void test()" << endl;}
private:int _a;
};
int main()
{
//匿名对象的生命周期只有所在行,离开所在行就会被析构掉;A();//这是一个匿名对象A(1);//这也是一个匿名对象A(A(1));//这也是一个匿名对象return 0;
}

编译器的一些优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。
演示代码:

class A
{
public:A():_a(0){cout << "A()" << endl;}A(int a) :_a(a){cout << "A(int a)" << endl;}A(const A& a):_a(a._a){cout << "A(const A& a)" << endl;}~A(){cout << "~A()" << endl;}void test(){cout << "void test()" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)"<<endl;_a = a._a;return *this;}
private:int _a;
};
void fun1(A a)
{}
A fun2()
{A a;return a;
}

首先我们来看第一组
在这里插入图片描述
正常情况下aa1调用A()构造函数,调用fun1然后aa1传参给形参,形参会调用拷贝构造函数
最后程序结束;
我们来看看结果是不是这样:
在这里插入图片描述
这个没什么可说的,也没有可以优化的地方,是正常情况!
我们再来看看第二组
在这里插入图片描述
首先程序先去调用fun2,来到fun2内部,调用A()构造函数初始化a对象,然后返回a对象;
一般情况下,对于这种值返回的函数并不是直接将值返回,而是按照下面这种方式:
在这里插入图片描述
那么综上所述:编译器先调A()构造函数初始化a对象,然后再调用的拷贝构造函数初始化的临时对象;
通过程序验证一遍:
在这里插入图片描述
我们会发现编译器并没有调用拷贝构造函数,这是为什么呢?
主要就是因为编译嫌麻烦!你看啊,编译器都可以用返回值来初始化临时变量的,那么为啥不直接用返回值来构造接受对象呢?
也就是说在编译器看来,一个表达式中,连续构造+拷贝构造->优化为一个构造,编译器先麻烦,对其进行了优化;省去了中间商赚差价(临时变量消耗时间),有利于提高程序运行效率;
为此我们没有看到拷贝构造函数;
第三组:
在这里插入图片描述
这是int类型传给A类型,会出现隐式类型转换,一般情况如下:
在这里插入图片描述
也就是说在一般情况下,我们会看到编译器先调用A(int)构造函数,再调用拷贝构造函数
但是这么绕圈的方式,太麻烦了,编译想一步到位,根据 “一个表达式中,连续构造+拷贝构造->优化为一个构造”的规则,编译器就直接拿着这个1去构造形参,减少了中间商赚差价,提高了程序运行效率;
为此经过优化过后,编译器只会去调用A(int)构造函数来实现(int)到(A)的类型转换;
在这里插入图片描述

第四组:
在这里插入图片描述
一般情况:
在这里插入图片描述
根据“一个表达式中,连续构造+拷贝构造->优化为一个构造”的规则
编译器会直接拿着1去构建形参对象a,因此编译器只会调用A(int)构造函数来完成此次类型转换!不会生成匿名对象;
运行结果:
在这里插入图片描述
第五组:
在这里插入图片描述
我们来仔细推敲一下:
在这里插入图片描述
也就是说从return语句开始:调用临时对象的拷贝构造函数、调用aa1的拷贝构造函数,这样的连续拷贝让编译器受不了了,编译器对其进行了优化,直接直接绕过临时变量这个中间商,直接调用aa1的拷贝构造函数,将a对象的值拷贝给aa1;
为此我们最后看到的是编译器只会:调用A()构造函数(初始化a对象)、拷贝构造函数(初始化aa1);
对于一些激进的编译器,会将对象a的构造函数也优化了,直接拿初始化a的值来初始化aa1;
为此我们看到的就只有编译器调用A(),这种比较激进的结果在VS2022下可以看到:
在这里插入图片描述
在这里插入图片描述

一般来说编译器是不会将A a调用构造函数也加入优化,因为和reture语句都不是同一行,贸然的优化进来会出现风险和bug!
我们拿比较稳妥的结果,总结出一套优化结论
一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造

第六组:
在这里插入图片描述
在比较稳定的编译器(不激进的编译器)下,这个过程是无法优化的,因为既满足:一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造的规则,也不满足“一个表达式中,连续构造+拷贝构造->优化为一个构造”的规则,编译器只能老老实实的按照原样走:我们就会看到编译器调用:A()、A()、A(const A&)、A& operator=(const A& a)这些函数;
在这里插入图片描述
但是对于一些比较激进的编译器,他们会优化掉拷贝给临时对象的过程,直接拿返回值赋值给接受对象,VS2022下就是这样做的:
在这里插入图片描述

总结:
1、为了提高程序运行效率,参数传递尽量用引用,尽管使用值传递的方式大多有优化,但是都需要去调用构造函数,会比较浪费时间,使用引用的话可以避免拷贝、调用构造函数;
2、尽量使用定义对象的方式接受值传递的返回值,避免使用赋值的方式接收值传递的返回值;通过对比第五组、第六组函数调用,明显使用第五组的方式接受返回值的效率更高!(我们讨论的是在不激进的编译器下的优化)
3、尽量使用匿名对象充当返回值;


http://www.ppmy.cn/news/23637.html

相关文章

云原生安全2.X 进化论系列|揭秘云原生安全2.X的五大特征

随着云计算技术的蓬勃发展&#xff0c;传统上云实践中的应用升级缓慢、架构臃肿、无法快速迭代等“痛点”日益明显。能够有效解决这些“痛点”的云原生技术正蓬勃发展&#xff0c;成为赋能业务创新的重要推动力&#xff0c;并已经应用到企业核心业务。然而&#xff0c;云原生技…

《唐诗三百首》数据源网络下载

2023年的 元宵之夜&#xff0c;这场以“长安”为主题的音乐会火了&#xff01;在抖音&#xff0c;超过2300万人次观看了直播&#xff0c;在线同赏唐诗与交响乐的融合。许多网友惊呼&#xff0c;上学时那些害怕背诵的诗句&#xff0c;原来还可以有这么美的表达这场近80分钟的音乐…

谈谈Spring中Bean的生命周期?(让你瞬间通透~)

目录 1.Bean的生命周期 1.1、概括 1.2、图解 2、代码示例 2.1、初始化代码 2.2、初始化的前置方法和后置方法&#xff08;重写&#xff09; 2.3、Spring启动类 2.4、执行结果 2.5、经典面试问题 3.总结 1.Bean的生命周期 1.1、概括 Spring中Bean的生命周期就是Bean在…

Negative Prompt in Stable Diffusion

必读链接&#xff1a;https://www.reddit.com/r/StableDiffusion/comments/z7salo/with_the_right_prompt_stable_diffusion_20_can_do/ A lot of people have noticed that Negative Prompt works wonders in 2.0, and works even better in 2.1. Negative hints are the op…

怡合达业务大规模容器化最佳实践

作者&#xff1a;肖念康&#xff0c;东莞怡合达智能制造供应链资深 Java 开发工程师&#xff0c;主要负责公司内部 DevOps、代码托管平台、任务需求管理平台的研发及其他项目的管理&#xff0c;云原生的研究与开发工作。 公司简介 怡合达致力于自动化零部件研发、生产和销售&am…

【笔记】移动端自动化:adb调试工具+appium+UIAutomatorViewer

学习源&#xff1a; https://www.bilibili.com/video/BV11p4y197HQ https://blog.csdn.net/weixin_47498728/category_11818905.html 一、移动端测试环境搭建 学习目标 1.能够搭建java 环境 2.能够搭建android 环境 &#xff08;一&#xff09;整体思路 我们的目标是Andr…

雪花算法snowflake

snowflake中文的意思是 雪花&#xff0c;雪片&#xff0c;所以翻译成雪花算法。它最早是twitter内部使用的分布式环境下的唯一ID生成算法。在2014年开源。雪花算法产生的背景当然是twitter高并发环境下对唯一ID生成的需求&#xff0c;得益于twitter内部高超的技术&#xff0c;雪…

【逐步剖C】-第六章-结构体初阶

一、结构体的声明 1. 结构体的基本概念 结构体是一些值的集合&#xff0c;这些值称为成员变量。结构体的每个成员可以是不同类型的变量。结构体使得C语言有能力描述复杂类型。 如学生&#xff0c;有姓名、学号、性别等&#xff1b;如书&#xff0c;有作者&#xff0c;出版日期…