目录
一、再谈this指针
(1)this指针的引出
(2)this指针的特性
(3)this指针的小题目
1.下面程序编译运行结果是?
2.下面程序编译运行结果是?
二、const成员
(1)const的引入
(2)const的用法
(3)总结:
三、流插入和留提取的重载函数
(1)前言
(2)留提取的重载函数
(3)流提取的重载函数
四、再谈构造函数
(1)构造函数赋值
(2)初始化列表赋值
(3)关于初始化列表的注意点
1.每个成员在初始化列表中只能出现一次(初始化只能初始一次)
2.类中包含以下成员,必须在初始化列表进行初始化:
3.要尽量使用初始化列表,因为不管你是否使用初始化列表,对于自定义类型,都会先试用初始化列表进行初始化
4.成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的顺序无关
五、友元
(1)定义
(2)友元函数
(3)友元类
(4)内部类
六、static成员
(1) 概念
(2)特性
七、拷贝对象时候编译器的一些优化
我们先来定义一个日期类
//日期类的定义
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year / _month / _day << endl;
}
private:
int _year;
int _month;
int _day;
};
那么对于上面的代码,我们其实是有个问题的,就是Date类中的两个函数并没有关于不同对象的区分,那么当d1调用函数的时候,编译器是如何知道是哪一个对象调用的呢?
其实在c++被设计的时候就考虑到了这个问题,他通过this指针来解决了这个问题,即:c++给每一个“非静态的成员函数”都增加了一个隐藏着的this指针,这个this指针在每一个成员函数形参的第一个位置,当有对象去调用这个函数的时候,编译器会把这个对象的地址传递给该函数,然后通过这个地址去达到相应操作效果的。
这个操作逻辑其实在我们c语言中是最为常见的,因为我们无时无刻都需要传递一个变量的地址给函数,函数才能有相应的操作,只不过到了c++中,他把这个传地址的操作让编译器解决了,我们不需要传递罢了,但是实际上编译器在运行的时候还是通过this指针去解引用得到数据的的。
1.this指针的类型:类类型* const this
比如在我们的日期类中就是 Date* const this,但是因为他对于用户是透明的,我们看不到罢了。而这个const是让this不能再函数中被改变,想想为什么?因为我们在用对象调用这个函数的时候,已经确定了就是这个对象,如果让this改变指向不就成了野指针吗?所以c++在设计的时候就已经保证了这一点。
2.只能在成员函数的内部使用,在成员函数内部可以显式的调用,比如*this等,但是在形参列表中不能显式的写出来。
3.this指针的本质就是一个被隐藏起来的形参罢了,当对象调用函数的时候,编译器自动把对象的地址传递给this指针,所以this指针并不是存储在对象中的
4.this指针是形参中的第一个,一般情况下由编译器通过ecx寄存器自动传递,并不需要用户手动传递地址。为什么用寄存器传递的?因为要访问对象中的数据都需要使用this指针解引用,所以编译器认为这是一个很频繁的操作,所以把this指针存储在寄存器中提高效率。
【面试题】
1.this指针存在哪里?
2.this指针可以为空吗?
(1)this指针就是一个被隐藏起来的形参,在调用函数的时候由编译器自动传递,所以他和普通的形参一样,都是在栈帧中。
(2)this指针可以为空,当我们在调用某个函数的时候,但是这个函数并不需要去解引用this指针,比如他只会打印一句话等等。但是一旦我们要访问对象中的元素,就需要this指针的解引用,这个时候是不能为空的
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
这道题的p是一个A类型的空指针,在调用Print函数的时候,编译器会把nullptr传给this指针,但是我们可以看到在Print函数的内部,并没有对this指针的解引用(因为没有访问对象中的任何数据)所以这个时候就仅仅是完成了一个形参的传递,但是并没有解引用,所以并不会报错,而是正常运行。
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<PrintA();
return 0;
}
那么这道题就不一样了,因为他涉及到对对象中数据的访问,所以他其实是this->_a,这样就是对this指针解引用了,然而我们又知道this指针却是nullptr,所以就是对空指针的解引用,就会出现运行崩溃。
我们将const修饰的成员函数称为const成员函数,表面上看const是写在成员函数形参列表外面的,即修饰成员函数的,但实际上这个const是修饰形参列表中所隐藏的this指针,表示该成员函数不能对对象的任何成员进行修改
有人可能会觉得为什么要设计这样特别怪异的语法,但其实是迫不得已,因为我们在上面说到了this指针是由编译器自动传递的,我们不能在形参列表中显式的写出来,(编译器的this指针其实是类const* 类型的,在上面提到过)所以我们要想这个this指针不能对对象的任何成员变量进行修改就需要让他变成 const 类 const* 类型,但是由于我们不能显式的写,所以创造c++的人才不得已让const写在形参列表的外面,表示修饰这个this指针
我们来看看下面的代码
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
在这里我们写出了两个Print函数,其实就是给const对象和非const对象使用的,对于d1,因为他是Date 类型的非const对象,所以编译器会让他走最为匹配的print函数,即非const函数(上面的一个),但是对于d2来说,他是const定义的对象,所以他不能去走第一个Print函数,只能走下面一个const修饰的Print函数
这样说可能不是很好理解,但是我们只需要记住一点即可,const修饰的是this指针,而非函数本身,能不能调用这个函数,取决于你的对象是否和这个函数的this指针类型匹配!
不过值得注意的一点是,权限是可以缩小的,但是不能放大,也就是说在没有提供const函数的时候,d1也是可以调用const修饰的Print函数(权限的缩小);反过来在没有提供const函数时,d2不能调用非const修饰的普通成员函数(权限的放大)
思考问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
解答:
(1)const对象不能调用非const成员函数,以为this指针的类型不匹配,一个是
const 类 const* 另一个是 类 const* 这会导致权限的放大,所以不行
(2)非cost对象可以调用const成员函数,以为这是权限的缩小
(3)const成员函数内不可以直接调用其他的非const成员函数,因为直接调用的本质是this->函数,但是const成员函数的this是被const修饰的,直接调用非const会造成权限的放大,所以不行。但是你如果传参进来一个普通的对象,然后用个这个对象的指针去调用非const成员函数是非常可以的,即权限的平移
(4)非const成员函数内可以直接调用其他的const成员函数,即权限的缩小
不需要记住太多的规律,只需要知道两点:
(1)看对象和this指针的类型是否匹配
(2)权限是可以平移,可以缩小的,唯独不能放大
一般而言const都是用来修饰只读函数的,防止用户不小心修改了成员变量的数据,但是通常也会提供给两个版本的函数,一个给const对象使用,另外一个给非const成员使用,这就不会出错了
对于运算符重载,我们在之前的文章中已经完成了重载,运算符重载的提出,让我们的自定义类型的操作变得更加方便,并且非常符合我们的使用习惯,但是对于流插入和留提取函数却不能简简单单的写在类中,因为类中的成员函数第一个形参都是this指针,并非istream或ostream的对象,所以这就导致了,如果我们把这两个函数写在类中作为成员函数的话,就与我们的使用习惯相反了,所以需要把重载函数写在类外面,让成员函数this指针的特点不会影响到我们,但是这样也会有个问题:类外不能访问私有的成员变量,于是我们又用到友元声明,告诉编译器我这个函数是你类的朋友,可以访问类中的任何成员
受到成员函数this指针的特性的影响,我们不能写两个形参
如果写在类中,会与我们的操作习惯不符(下图)
正确的写法应该是这样的,在类外定义一个重载函数,(就不会因为成员函数的特性而使得this指针一直抢占第一个形参为止)然后用友元,让这个函数可以访问私有的成员变量,这样才可以符合我们的操作习惯重载出一个类的流插入重载函数了
class Date
{
public:
//构造函数
Date()
:_year(2023)
,_month(12)
,_day(26)
{}
//友元
friend ostream& operator<<(ostream& out, Date& d);
private:
int _year;
int _month;
int _day;
};
//流提取的重载
ostream& operator<<(ostream& out,Date& d)
{
out << d._year <<"/"<< d._month <<"/"<< d._day << endl;
return out;
}
这里返回out是因为我们普通的cout是可以连续多次的使用流提取的,返回out后就可以与普通的一样,更加符合我们的操作习惯
流插入的重载与留提取十分类似,我们在这里直接给出结果
class Date
{
public:
//构造函数
Date()
:_year(2023)
,_month(12)
,_day(26)
{}
//友元
friend ostream& operator<<(ostream& out, Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
//流提取的重载
ostream& operator<<(ostream& out,Date& d)
{
out << d._year <<"/"<< d._month <<"/"<< d._day << endl;
return out;
}
//流插入的重载
istream& operator>>(istream& in,Date& d)
{
in >> d._day >> d._month >> d._day;
return in;
}
所以我们在操作符重载的时候,如果涉及到不符合我们的操作习惯的,不妨尝试用友元的方式,在类外面定义一个函数,就可以逃过成员函数this指针的限制了
在之前,我们通过调用构造函数来创建对象的时候,在构造函数的函数体中给各个成员变量赋予了一个合适的初始值。像这样:
虽然这样写,我们在实例化一个对象的时候,编译器会自动调用构造函数去给对象赋值,但是我们不能将其称为对对象的成员的初始化,因为构造函数函数体中的语句只能将其称为赋初值,而不是初始化,真正的初始化是在初始化列表中的,且初始化只能进行一次,但是构造函数体中可以进行多次赋值。
初始化列表:属于构造函数的一部分,写在构造函数函数体之前,通常用一个冒号开始,接着用逗号分隔成员数据,每一个成员变量后跟一个放在括号中的初始值或者表达式。
从这张图片中,我们可以看出,初始化列表是在构造函数函数体前执行的,而且在进入构造函数函数体内部的时候,初始化列表其实已经会对成员变量进行第一次赋值(c++并没有规定赋值成多少,一般都是随机值,因为编译器对内置类型不会处理,如果你显示的把内置类型写到初始化列表中,编译器默认就把他初始化成0了)
就像在这里,我们没有显示的写出来_month于是他就是随机值
所以到这里,我们对于初始化列表和构造函数的理解可以加深一些了:我们之前所说的编译器不会对内置类型进行处理指的是,在初始化列表中编译器不会对内置类型进行初始化(当然我们也说过c++11规定可以在声明的时候给缺省值,其实这个缺省值就是让编译器在初始化列表阶段对内置类型进行初始化处理)像这样我们虽然没有在初始化列表写出内置类型_month的初始化,但是我们有了缺省值,所以在编译器执行到初始化列表的时候,就会改变规则(改变对内置类型不处理的规则),让内置类型也能够在初始化列表的时候有一个合适的初始值(即缺省值)
在初始化列表阶段,如果没有显示写,内置类型会被初始化成随机值,自定义类型会调用其默认构造函数。
为什么要有初始化列表?
因为有的类中包含了const成员/引用类型变量/没有默认构造函数的自定义类型,而初始化列表就是对象定义的地方,(const变量和引用类型变量在定义的地方必须要赋初值),如果像c语言的结构体一样在创建对象的时候定义好了,那么const变量和引用类型对象就不能赋值了,出于这个考虑,c++祖师爷才发明了初始化列表,让每一个const变量和引用类型对象都能享受到实例化对象的时候再赋值的权利,这才让类变得更加方便了
(1)引用成员变量
原因:引用类型不能重新引用别的变量,也就是说引用必须要在定义的时候初始化
所以引用类型必须要在初始化列表赋值(但是由于引用类型不能先声明在定义,所以不能显示的写在初始化列表中,而是通过缺省值的方式间接走初始化列表)
(2)const成员变量
原因:const对象一旦创建后就不能修改,所以const对象必须要初始化
不论你是给缺省值,(最终还是走到初始化列表)还是显示的写到初始化列表中,反正const对象必须要初始化
(3)自定义类型成员,且该自定义类型没有默认的构造函数
原因:因为该类自定义类型没有默认的构造函数,如果在初始化列表中不显示的写出来,就会导致该类型没有走构造函数(因为自定义类型编译器不会默认生成构造函数)
在这里,我们写了一个构造函数,但是该构造函数不是默认构造函数(无参构造函数、全缺省构造函数、编译器自动生成的默认构造函数都被认为是默认构造函数),这个时候编译器就不会再生成默认构造函数了,于是我们在Date类中使用Time的时候,就会报错:Time不存在默认构造函数。所以当成员没有默认构造函数的时候,必须要在初始化列表中写出其构造函数。
其实初始化列表就是缺省参数和调用默认构造使用的地方,你在声明时候给的缺省参数,和自定义类型的默认构造函数都是先进入初始化列表赋初始值,然后再进入构造函数的函数体中进行修改的
可以看到,尽管我们没有在Date中显示的写初始化列表,但是Time仍然会走一次自己的默认构造函数(其中包含了初始化列表)
这个结果就能很好的说明上述事项,因为刚开始a1和a2都是随机值,由于a2先声明,所以a2先初始化成随机值,a1后声明,所以a1后被初始化成1
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
问题:在上面尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
由于我们在上面已经详尽的讲述了友元函数的用法,在这里只给出总结
总结:
(1)友元函数可访问类的私有和保护成员,但不是类的成员函数
(2)友元函数不能用const修饰(因为没有this指针)
(3)友元函数可以在类定义的任何地方声明,不受类访问限定符限制
(4)一个函数可以是多个类的友元函数
(5)友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(1)友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
(2)友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
(3)友元关系不能继承,在继承位置再给大家详细介绍。
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. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
static函数虽然属于类,但是和普通的成员函数不一样,只是受到类的访问权限限制,但是他其实是类似于全局函数的东西,没有this指针,所以不用创建对象就能够调用静态成员函数。静态成员函数的出现,让我们调用成员函数变得更加方便准确了。
(1)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
(2)静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明, 因为类中给的都是缺省值,但是静态成员不会走初始化列表,所以static成员不能够给初始值(缺省值)
static修饰变量,其在静态区开辟空间,该变量只会被初始化一次。如果是在类里面初始化,那是不是每创建一次对象都需要初始化一次static变量。所以在类里面只是定义,类外初始化。static const 比较特殊,属于常量,可以直接在类里面初始化。(但是仅仅只有整形才是这个特例)
由此可见,类里面的变量一般是声明,没有定义,如int&类型也是在类里面声明,在构造函数的时候初始化就行了
(3) 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
(4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员
(5) 静态成员也是类的成员,受public、protected、private 访问限定符的限制
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
总而言之:编译器的优化需要两点要求:
(1)在一行连续的步骤中
(2)同种类型的构造,比如构造函数和拷贝构造