Hello,大家好,今天,我们来继续学习类和对象部分的知识。
目录
1.类型转换
2.再探构造函数(初始化列表)
3.static成员
4.友元
5.内部类
6.匿名对象
7.对象拷贝时的编译器优化
1.类型转换
(1).C++支持内置类型隐式类型转换为类类型对象,需要相关内置类型为参数的构造函数。
先看代码:
#include<iostream>
using namespace std;
class date
{
public:date(int a1){cout << "date(int a1)" << endl;_a1 = a1;}date(date& d){cout << "date(date& d)" << endl;_a1 = d._a1;_a2 = d._a2;}void print(){cout << _a1 << " " << _a2 << endl;}
private:int _a1;int _a2;
};
int main()
{date d = 1;date d1 = 3.3;d.print();//1 -858993460return 0;
}
"date d = 1;"这一步的操作是隐式类型转换,1是整数类型,在进行这一步操作时,编译器会自动将1这个int类型的值转换为date类型,将其存放到一个临时对象中(换句话说,就是编译器会构造一个临时对象),然后,编译器会将这个临时对象拷贝构造给d。"date d1 = 3.3;"在进行这一步操作时,编译器会先将3.3转换为整数,然后再构造临时对象(注意:不是所有的类型都支持类型转换,比如字符串就不可以)。
编译器在进行隐式类型转换这一步操作时,编译器会先调用构造函数date(int a1),也就是以内置类型变量_a1变量为参数的构造函数,创造一个临时对象,创建好后,最后,再调用拷贝构造函数将这个临时对象拷贝到date类型的d对象中,这样,就完成了隐式类型转换操作。
这里我们再补充一个东西:就是编译器再进行这一步操作时,会做优化操作,当编译器遇到连续构造+拷贝构造-->优化为直接构造,上述的类型转化过程中会被直接转化为直接构造。按照我们上述所讲的内容来说,我们这里的运行顺序应该是先调用构造函数,接着再去调用拷贝构造函数,应该会打印出两行的内容到屏幕上,但是,这里却只打印出了一行,也就是这里只调用了一次构造函数,而且还没有报错,这足以说明编译器在这里进行了优化操作,也就是说,编译器在执行到这一步操作时,就直接够造出了临时对象,这个临时对象编译器就会将它当作d这个对象,这就是编译器自动进行的优化操作。
注:以上的隐式类型仅仅限于单参数转化,C++11之后才支持多参数转化。
如果我们想要实现多参数转换,就要加上一对{ }括号。例(这里以双参数转换为例):
date d = {1,3};
#include<iostream>
using namespace std;
class date
{
public:date(int a1){cout << "date(int a1)" << endl;_a1 = a1;}
private:int _a1;int _a2;
};
class stack
{
public:void print(date aa){cout << "void print(date& aa)" << endl;}
private:int _st1;int _st2;
};
int main()
{stack st;st.print(1);return 0;
}
这样的话,就可以更方便完成上述的这一步种情况了。讲到这里,想必大家对类型转换有了一定的了解了,这里给大家说一下,就是C++设计这个类型转换其实就是为了方便使用。
(2).构造函数前面加上explict就不在支持隐式类型转换了。
class date
{
public:explict date(int a)//不可以将int类型转换date类型了 { }
};
2.再探构造函数(初始化列表)
(1).之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有另一种方式,就是初始化列表,初始化列表的使用方式时以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 " 成员变量 " 后面跟一个放在括号中的初始值或者表达式。
class date
{
public:date(int year, int month, int day):_year(year),_month(month),_day(1+2+3)//year、month是初始化值,_year、_month、_day是成员变量,"1+2+3"是一个表达式{ }
private:int _year;int _month;int _day;
};
(2).每个成员变量在初始化列表中只能出现一次,在语法上理解初始化列表可以认为是每个成员变量定义初始化的地方。
date(int year, int month, int day):_year(year), _month(month), _day(day)//,_year(year)//这里不可以写这句代码,我们前面已经都定义过一次了,若将这句代码写上的话,编译器会报错,因为这种情况属于是重定义,_year在前面已经定义过一次了。
{ }
这里我们再来强调一个知识点,我们在main函数中定义了一个date类类型的对象d,当系统运行到这里时,系统会按照它的成员变量为d这个对象申请相应的空间,注:这里仅仅是将空间给申请好了,并没有去定义各个成员变量,成员变量的定义是在初始化列表中定义的。也就是说,这里只是为d对象开创好空间了,并没有为其中的成员变量完成初始化,只有在初始化列表中是真正的在进行定义操作。调用一个对象的构造函数进行初始化操作时,即便我们没有写有关初始化列表的东西,编译器它也会自动为我们生成对应的初始化列表,只不过各个成员变量的初始化值是随机值,不能使用。这里理解稍微有一点点抽象,大家有什么不会的可以在评论区问我,或者是私信我也可以。
#include<iostream>
using namespace std;
class date
{
public:date(int year, int month, int day): _year(year)//这里才是对各个成员变量进行定义的地方, _month(month)//我们这里没有写_day的定义操作,没写并不代表_day这个成员变量就没有定义,编译器会自行对_day进行定义,只不过初始值是随机值,不可以使用。{ }
private:int _year;int _month;int _day;
};
int main()
{date d(2024, 8, 30);//这里这是编译器为d这个对象开创了它所需要的空间大小,并没有对这个对象中的各个成员变量进行定义,也就是我们还需要在初始化列表中进行对各个成员变量进行定义操作return 0;
}
(3).引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
#include<iostream>
using namespace std;
class Time
{
public:Time(int hour)//这个函数是构造函数,它不是默认构造函数,当一个类中有构造函数的话,那么,编译器就不会再为这个类默认生成一个默认构造函数。{_hour = hour;}
private:int _hour;
};
class date
{
public:date(int year, int month, int day, int& n):_t(0), _n(n), _a(1)//_t、_n、_a1这三个成员变量必须在初始化列表中进行初始化操作,若在其他的地方进行初始化操作的话,编译器就会报错,因为这三类成员变量必须要在定义的同时进行初始化操作。{_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;Time _t;//没有默认构造的类类型变量int& _n;//引用成员变量const int _a;//const成员变量
};
这三种情况就是要求必须在初始化列表中进行初始化操作,因为它们初始化的要求就是必须在定义的同时进行初始化操作。
1>.const修饰变量进行初始化操作:
解析如下所示:我们前面先定义了一个const类型的变量a,定义了这个变量之后,那么a这个变量就是只能读不能修改了,既然如此,我们下面的那一步 "a=1" 这一个步骤,就相当于是要改变a这个变量的值,由于a是被const修饰过了,因此我们这里就不可以被修改,所以编译器会报错。
2>.引用成员变量:
我们前面讲过,就是对于引用成员变量,它在定义变量的时候必须要进行初始化操作,否则系统就会报错,如果我们这里不初始化的话,那我们大家想一下,这种情况下我们应该引用谁?这样像一下,就会觉得这里就必须进行初始化操作。如果大家不能理解的话,大家只需要记住就好了,就全当这是编译器的一个规定就可以了。
3>.没有默认构造的类类型变量:
我们这里结合上面我们所写的那个代码来讲解一下, 通过我们上面的代码可知,我们并没有为Time类写一个默认构造函数,而是写了一个构造函数,这个构造函数需要我们传值去构造,通过我们前面的构造函数定义可知当我们创建好一个对象的时候,就会去调用构造,换句话说,就是构造函数是在定义的同时去调用的,对于没有默认构造的类类型变量而言,我们就要在定义的地方去调用构造函数,既然如此,就必须在初始化列表中去调用构造函数对于没有默认构造的类类型变量来说。
除此之外,我们这里的初始化列表和函数体赋值也可以混着使用:
class date
{
public:date():_ptr((int*)malloc(sizeof(int) * 12)){if (_ptr == nullptr){perror("malloc fail");return;}}
private:int* _ptr;
};
就比如说我们上面这个,我们是在初始化列表中是创建了一个空间,然后我们在函数体中去判断一下这个空间是否被我们开创成功。这样写是完全正确的。
(4).C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
class date
{
public:date(int year = 2, int month = 2, int day = 2):_year(year),_month(month)//我们这里没有写为_day这个成员变量进行初始化操作,按照我们前面所讲的来说,这里编译器会自动为_day这个成员变量赋一个随机值,当然,这种随机值是在我们没有缺省值的情况下所赋的值,如果我们这里给了缺省值的话,编译器就会使用缺省值为_day这个成员变量进行初始化操作。{ }
private:int _year = 1;int _month = 1;int _day = 1;//这三个成员变量等号后面的这个值就是我们前面所说的缺省值。int*arr=(int*)malloc(12);//不仅如此,成员变量不仅仅只能是值,还可以是一个表达式。
};
写到这里,我们在这里还需要去注意一个东西,就是我们有的小伙伴在看了这里的这个缺省值后将成员变量的缺省值和形参的缺省值给搞混了,这里我们就下面这个代码来给大家强调一下。
#include<iostream>
using namespace std;
class date
{
public:date(int year = 1, int month = 1, int day = 1):_year(year),_day(day){ }
private:int _year = 2;int _month = 2;int _day = 2;
};
int main()
{date d1(2024, 9);return 0;
}
解释:这里我们先定义了一个date类型的变量d1,我们在date类中写了一个构造函数我们将这个构造函数的三个形参都给了缺省值,我们传过去的实参只穿了两个,也就是说,我们这里的day是使用缺省值,与此同时,我们又在date类型的类中定义的成员变量也都给了缺省值,在这里给缺省值就是说,如果在初始化列表中我们没有使用构造函数的参数进行初始化操作的话,那么这个成员变量就会使用缺省值进行初始化操作。
解释完了,我们来看代码:我们传过去的是(2024,9),2024会给year,9会给month,day则使用缺省值1,此时程序会进入初始化列表中去进行初始化操作,_year这个成员变量初始化为2024,_day这个成员变量初始话为9,此时我们再看_month,对于这个成员变量,我么并没有在初始化列表中对其进行初始化操作,此时,我们再来看声明成员变量的地方,我们发现我们在声明成员变量的时候,将这个成员变量赋予了缺省值2,那么,根据我们前面所讲的知识来看,我们没有在初始化列表中对_month进行初始话操作的话,编译器就会对_month这个成员变量成员在声明时候所定义的那个缺省值进行初始化操作,在这里也就是2这个值进行初始化操作。
(5).尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员变量也会走初始化列表,如果这个成员变量在声明的地方给了我们1缺省值的话,初始化列表会用这个缺省值进行这个成员的初始话。如果你没有给缺省值的话,那么对于没有在初始化列表显示初始化操作的内置成员变量是否初始化就完全取决与编译器,C++在这里没有规定。对于没有在初始化列表显示初始化操作的自定义成员变量会调用这个成员类型的默认构造函数,如果内有默认构造函数的话编译器就会发生编译错误。
总结:每个构造函数都有初始化列表:
每个成员变量都要走初始化列表
1).在初始化列表初始化的成员变量 显示写的初始化成员。
2).没有在初始化列表初始化的成员 没有显示写的初始化成员。
a.声明的地方如果有缺省值的话用缺省值进行初始化操作。
b.没有缺省值。
x:内置类型,不确定,看编译器,大概率是随机值。
y:自定义类型,调用这个自定义类型成员变量的默认构造函数,若没有默认构造函数就报错。
3).引用成员变量,const成员变量,没有默认构造的类类型变量这三者必须在初始化列表进行初始化操作。
(6).初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表中出现的先后位置无关(建议声明顺序和初始化列表的顺序保持一致)。我们来看下面的代码:
class date
{
public:date(int year, int month, int day):_year(year),_day(_month),_month(month){ }
private:int _year;int _month;int _day;
};
通过我们的上述代码的运行结果来看的话,这和我们想象的结果完全不一样,解析如下所示:
这是因为在初始化列表中成员变量初始化的顺序和成员变量的声明顺序是一样的,我们通过上面的那幅图可知,我们创建了一个date类型的对象,传过去的实参是(2024,9,6),接下来我们看date类,我们发现在date类中,我们为其中的3个成员变量的声明顺序是_year,然后是_month,最后才是_day,按照我们的初始化列表中成员变量初始化的顺序和成员变量的声明顺序是一样的这个规则去走程序的话,我们可以知道在初始化列表中是先给_year这个成员变量进行初始化操作,初始化为2024,然后是给_month这个成员变量进行初始化操作,初始化为month(9),最后一个就是给_day这个成员变量进行初始化操作,初始化为_month(9),综上所述,我们最后输出的就是2024 9 9,如果我们这里是按照初始化列表中的顺序进行初始化操作的话,那么输出的_day就应该是随机值,而不是这里的9了。
补充:类类型的对象在是实例化后其实就有空间了,里面的空间其实也就包括各个类成员变量的空间,只不过在构造函数的初始化列表里面才算是真正定义的地方,这不过这个时候就不会再成员开新的空间了。(这里再说一下这个知识其实主要就是想让大家理解这个类类型的变量是在初始化列表中进行定义的,害怕有一些同学将这个声明和定义给搞混了)。
3.static成员
(1).用static修饰的成员变量,称之为是静态成员变量,静态成员变量一定要在类外面进行初始化操作。
(2).静态成员变量为所有的类类型的对象所共享(不同类型的对象也可以使用),不属于某个具体的对象,它不存在于某一个对象中,它存放于静态区中。
通过上述的sizeof函数求出的d对象的大小我们可知static修饰的成员变量确实不在对象中。
(3).用static修饰的成员函数,称之为是静态成员函数,它没有this指针,不可以访问静态成员函数,而非静态的成员函数(就是不被static修饰的成员函数),是可以访问任意的静态成员变量和静态成员函数。
(4).突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
(5).静态成员也是类的成员,也受public、prodected、private访问限定符的限制。
(6).静态成员变量不能在声明的位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
#include<iostream>
using namespace std;
class date
{
public:date(){_amount++;}static void add(){_amount++;cout << _amount << endl;}
private:static int _amount;//_amount这个静态成员变量受到private的限制,因此它不可以在对象外被访问
};
int date::_amount = 0;//static修饰的成员变量必须在类域外进行初始化操作。
int main()
{date d1,d2;//我们在这里定义了两个date类型的对象d1和d2,通过我们前面所学的知识可知,这里会调用构造函数,由于我们在这里是定义了两个对象,因此我们在date类中定义的static静态成员变量_amount就相当是加了两次1,也就是说,当编译器运行完这一句代码后_amount的值就变成了2了d1.add();//3;这里是调用d1对象中的add()这个成员函数,在这个函数中我们又让_amount这个成员变量加了一个1,然后将这个_amount打印出来,因此,我们在屏幕中就会看到打印出来的值是3return 0;
}
4.友元
(1).友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或类声明的前面加上friend,并且把友元声明放到一个类的里面。
(2).外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。(友元类也是如此)
(3).友元函数可以在类定义的任何地方声明,并且不受类访问限定符的限制。
(4).一个函数可以是多个类的友元函数。
#include<iostream>
using namespace std;
class date;//我们这里要先声明一下date类,否则当编译器执行到第7行代码的时候,它不认识这个date,就会报错
class tim
{
public:friend void print(date& dd, tim& tt);//print函数是tim类的友元函数tim(){_hour = 17;_minute = 58;_second = 24;}
private:int _hour;int _minute;int _second;
};
class date
{
public:friend void print(date& dd, tim& tt);//print函数即是date类的友元函数,也是tim类的友元函数date(){_year = 2024;_month = 9;_day = 7;}
private:int _year;int _month;int _day;
};
void print(date& dd, tim& tt)
{cout << dd._year << " " << dd._month << " " << dd._day << " " << tt._hour << " " << tt._minute << " " << tt._second << endl;
}
int main()
{date d;tim t;print(d, t);//2024 9 7 17 58 24return 0;
}
(这个友元函数随便写在类的的任何一个地方都可以,写的位置对这个效果不会产生影响。)
(5).友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
#include<iostream>
using namespace std;
class A
{
public:friend class B;//友元声明,就相当于是告诉编译器这个B这个类是A的友元类,B可以随便的去访问A中的私有和保护成员(包括成员函数和成员变量)
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:void func1(const A& aa)//func1()这个函数是A的友元函数{cout << aa._a1 << " " << _b1 << endl;}void func2(const A& aa)//func2()这个函数是A的友元函数{cout << aa._a2 << " " << _b2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};
int main()
{A aa;B bb;bb.func1(aa);//1 3bb.func2(aa);//2 4return 0;
}
(6).友元类的关系是单向的,不具有交换性,比如说A类是B类的友元类,但是B类不是A类的友元类。
(7).友元关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
(8).有时提供了便利。但是友元会增加耦合度,就是会破坏封装,所以友元不适宜多用。
5.内部类
(1).如果一个类定义在另一个类的内部,这个内部类就叫做内部类,内部类是一个独立的类,跟定义在全局相比,他只是受到外部类类域的限制和访问限定符的限制,所以外部类定义的对象中不不含内部类。
(2).内部类默认是外部类的友元类。
(3).内部类本质上也是一种封装,如果A类和B类紧密关联的话,且A类实现出来就是为了给B类使用的话,那么我们就可以考虑把A类设计为B类的内部类,如果放到private/protected的位置上,那么A类就是B类的专属内部类,其余地方均用不了。
#include<iostream>
using namespace std;
class A
{
public:class B//B是A的友元类{public:void print(const A& aa)//这个函数是A的友元函数{cout << aa._a1 << " " << aa._a2 << " " << _b1 << " " << _b2 << endl;}private:int _b1 = 3;int _b2 = 4;};
private:int _a1 = 1;int _a2 = 2;
};
int main()
{A aa;cout << sizeof(aa) << endl;//8;根据这个结果足以说明内部类是一个独立的类A::B bb;bb.print(aa);//1 2 3 4return 0;
}
6.匿名对象
(1).⽤类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参)定义出来的叫有名对象。
(2).匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名。
#include<iostream>
using namespace std;
class A
{
public:A(int a = 0):_a(a){ }void func(){cout << "void func()" << endl;}
private:int _a = 1;
};
int main()
{A aa;//有名对象,名字是aa//A aa1();//不可以这么定义对象,因为编译器无法识别这是一个函数声明,还是创建一个对象,有名对象不能这么写,但是匿名对象却可以这么写A();//匿名对象必须要这么定义,这种是不传参的A(1);//这种是传参的//接下来,我们用匿名对象和有名对象分别来调用一下func()函数A a;a.func();//void func()A().func();//void func()//通过上面的3行代码我们可知,同样都是调用func()函数,如果用有名函数调用的话,要写2行代码,若是用匿名对象调用的话,就只需要写1行代码,这样会很方便A().func();//这里我们需要注意一件事,就是匿名函数的生命周期只是在当前这一行return 0;
}
补充:临时对象的生命周期和匿名对象的生命周期一样,都是只在当前这一行。
7.对象拷贝时的编译器优化
(1).现代编译器会为了尽可能提高程序的效率,在不影响正常性的情况下会尽可能地减少一些传参和传返回值地过程中可以省略地拷贝。
(2).如何优化,C++标准并没有规定,各个编译器会根据情况进行自行处理。当前主流地相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会自动进行合并优化操作,有些更新更 " 激进 " 的编译器会进行跨表达式的合并优化。