学完这一节,你将更加理解什么是const引用,为什么要在this指针前面加上const修饰.
先给大家复习一下什么是const引用:
我们知道在函数传参的时候,为了减少拷贝,所以我们一般选择传引用的方式,但是传引用有一个问题,假如我不想任何人在函数内部改引用值,从而改变实参.我们一般都会选择在引用的基础上加上const修饰,这样的话,有两个优点:
总而言之:只要是在传引用的参数,且在函数内部不想改变引用值的函数,我们都建议使用const引用.
我们之前学过成员函数的第一个参数是this指针,他的类型是Date* 类型,const修饰的是指针,也就是this指针的指向不能改,但是this指针指向的内容是可以改的,以成员函数–打印函数为例:
Print(/*Date* const this*/)
类比我们上面的const引用,这里的this指针也是建议加上const,也就是使this指针也能达到上面列举的两个优点.
下面是Print()中this指针没有加上const修饰前,主函数中一个const修饰的对象调用Print()函数失败的报错信息:
但是这里又有一个问题:这里的this指针是隐含的,你先显式的写出this都不可以,更别提加上const修饰了
所以C++就提出了一个解决问题的语法支持:在函数头和函数体中间加上一个const修饰,这个const就在C++语法上加到了this指针类型的最前面,也就是:
Print() const
Print(/*const Date* const this*/) const
加上后:
学完了这一节,你将会使用cout<<输出自定义类实例化的对象,可以代替Print()函数的功能了.
问题a: 首先cout是什么东西?
其实cout是ostream类实例化出来的对象,我们并没有写ostream和cout但是它们都被包含在了头文件iostream中,所以可以直接使用.同理cin就是istream实例化出来的对象.
问题b: 那为什么我们cout可以使用<<连续输出所有内置类型的值呐?
ps:<<好像有两个身份:1.左移操作符 2.C++的输出操作符
ps:&好像也有两个身份:1.取地址 2.引用
这里有三个点:第一:之所以cout可以使用<<输出,那是因为ostream类中对运算符<<重载了
第二:之所以cout<<可以输出所有内置类型的值,也就是cout可以自动识别int类型还是char类型,不需要我们类似C语言printf格式化输出,那是因为C++中在对所有内置类型都使用了运算符<<重载,多个函数名相同,输出参数类型不同的运算符重载函数就构成了函数重载.第三:之所以cout可以连续输出(cout<
//cout<<5<<'h'的实现框架:
ostream& operator<<(ostream& out, int val)
{
//输出整型值val的代码
return out;
}
ostream& operator<<(ostream& out, char ch)
{
//输出字符类型的值ch的代码
return out;
}
//链式输出的实际调用:
cout.operator<<(5).operator<<'h';
int a=100;
cout<<a;//流插入
cin>>a;//流提取
通过上节我们知道:内置类型的<<运算符重载函数已经是被大佬写入ostream对象中,但是对于自定义类型是未定义的,毕竟自定义类型的成员变量的类型和顺序结构是千变万化的,大佬也没法管,所以对于自定义类型的运算符重载函数得自己写.
其实对于自定义类型的运算符重载函数的代码实现问题不大吗,难点在于这个函数的是写成ostream类成员函数,全局函数,Date类的成员函数,Date类的友元函数的一步一步的优化.
这种方式把自定义类型的运算符重载函数也和内置类型的运算符重载函数一样,写入ostream类中,把&cout传给了隐藏的this指针.
但是这里有一个尴尬的问题:ostream是通过包含头文件中包含进来的,所以没办法再把自定义类型的运算符重载函数写到ostream类中.
那么我们接着肯定就是考虑写成全局函数:
但是之前我们说过成员变量一般是封装成私有private修饰的:
private:
int _year;
int _month;
int _day;
这就是告诉我们没法在Date类域外访问_year等成员变量,所以写成下面这种全局的形式显然不妥当,总不能丢了西瓜(封装) ,捡了芝麻(自定义类型的<<运算符重载函数)吧.
//全局函数:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年"<< d._month << "月"<< d._day << "日" << endl;//_year等爆红
return out;//链式输出
}
吸收了上次写成全局函数没法访问私有的日期类的成员变量的经验,这一次,我们决定使用利用好封装,以其人之道还置于其人之身,我们把自定义类型的运算符重载函数写成Date类的成员函数:
但是我们发现这样还是没有解决问题,原因在于函数一旦写成Date类的成员函数,第一个参数一定时隐藏的this指针(Date* const this),this指针隐式的占用了<<操作符第一个参数的位置:
// 主函数调用代码:
Date d1(2022, 1, 1);
Date d1(2022, 12, 31);
cout << d1 << d2;
// Date类的实现代码:
ostream& operator<<(const Date & d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
但是这似乎还有一种反对初衷,不太合理补救措施——–调用的时候把d1对象和cout对象对调一下:
这样虽然能跑,但是有一些缺点:
到这里,如果大佬再不创造出语法上—-友元函数的支持,恐怕C++就不能支持自定义类型的流插入了.
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
#include
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& out,const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 12, 31);
cout << d1 << d2;
return 0;
}
ps:frind友元函数的声明可以放在Date类内的任意位置
cin的实现原理和cout类似,这里就不过多赘述.
学会这一节,你将知道成员变量是在初始化列表中被定义的和在某三种成员变量必须在初始化列表中初始化的.
首先我们知道类实例化出对象是在主函数中完成的(对象是整体定义的,但是并没有定义一个一个的成员变量),但是对象中的成员变量是在哪里被定义的呐?
实际上,所有的成员变量都要在初始化列表中定义.
总结一下: Date d完成的是一个对象的定义,调用构造函数完成的是一个对象d的初始化,那么要完成一个对象的初始化,我们就得使对象里面的成员都进行定义并且初始化.
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
以日期类中的成员变量的定义并初始化为例,来给大家看看初始化列表的格式:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
//初始化列表
:_year(year)
,_month(month)
,_day(day)
{
//_year = year;
//_month = month;
//_day = day;
}
其实像日期类的“这种”成员变量,我们可以在初始化列表中完成定义并初始化,也可以在初始化列表中只完成定义(这样的话,其实也没有必要显式的写出来),在函数体中完成赋值,就像下面:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
//:_year()//这个编译器自己也会
//,_month()
//,_day()
{
//初始化列表只能对每一个成员变量初始化一次,但是可以在函数体内反复赋值
_year = year;
_month = month;
_day = day;
}
ps:初始化列表只能对每一个成员变量初始化一次,但是可以在函数体内反复赋值
那么如果所有的类型的变量都可以像上面日期类的成员变量那样可以在在初始化列表中只完成定义(不用写),在函数体中完成赋值,那么初始化列表存在也毫无意义.
但是并非所有类型都是和他们一样,有下面三种成员变量是必须在初始化列表定义的时候就得初始化,所以下面三种是只能在初始化列表中完成初始化的(谁让你初始化列表是完成我的定义的地方,那么你就得送佛送到西,也得帮我完成初始化的任务)
回忆:
1.const类型的变量必须在定义的时候就初始化,之后就不能改了(const属性)
2.引用类型的变量必须在定义的时候初始化,之后就不能再引用其他变量了(之后就是赋值了)
ps:这里澄清一个概念问题:构造函数和默认构造函数和编译器生成的默认构造函数三个概念是不一样的
对成员变量分类:
内置类型:如果给了成员变量的缺省值,对象的默认构造函数就会在对象的默认构造函数的初始化列表中使用声明时给的缺省值,如果没给,就会时随机值
自定义类型:如果给了成员变量的默认构造函数,对象的默认构造函数就会在对象的默认构造函数的初始化列表中调用成员变量的默认构造函数,如果没给,就会报错.
的确会有点绕哈,但是看懂了就说明你学会了,哈哈哈哈
给了缺省值的情况,在初始化列表中使用缺省值的证明如下图:
class A
{
friend ostream& operator<<(ostream& out, const A& a);
public:
//默认构造函数:不用传参数就可以调用的构造函数,三种:全缺省,无参,编译器自动生成的
//如果不传参数,就没法调用,所以A(int a)这种半缺省的构造函数不是默认构造函数
A(int a)
{
_a = a;
}
private:
int _a;
};
int x = 2;
class B
{
friend ostream& operator<<(ostream& out, const B& d);
public:
B()
//三种特别的,如果内置类型的成员变量无缺省值
//,自定义类型的成员变量无默认构造,就必须在初始化列表定义的时候顺便初始化
:_b(1)
, _rx(x)
,_aa(3)
{
_ib = 4;
}
private:
int _ib;//普通类型
const int _b;//const修饰的成员
int& _rx;//引用类型的成员
A _aa;//没有默认构造的自定义成员
};
inline ostream& operator<<(ostream& out, const A& a)
{
out << a._a << endl;
return out;
}
inline ostream& operator<<(ostream& out, const B& d)
{
//内置类型的运算符重载在ostream类中已经定义了,直接用
out << d._ib << "---" << d._b << "---" << d._rx << "---";
//d._aa也是自定义类型,要输出也要写A类的运算符重载且定义成友元
cout << "---" << d._aa << endl;
return out;
}
int main()
{
B b;
cout << b << endl;
return 0;
}
问题:一个类必须提供默认构造函数,没有提供默认构造就会报错吗?
解答:错误
class A
{
public:
//这个是构造函数,但不是默认构造函数
//因为自己写了构造函数,所以编译器也不自动生成默认构造了
//也就是说这个A类没有提供默认构造函数,没有调用的话并没有报错,但是调用不传参就会报错.
A(int a)
:_a(a);
{
}
private :
int _a;
};
int main()
{
return 0;
}
建议:能用初始化列表,尽量使用初始化列表进行初始化.
因为:
规则: 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关.
猜一猜下面程序的执行结果是:
A. 输出 1 1
B .程序崩溃
C. 编译不通过
D .输出随机值 1
正确答案:D
class A
{
public:
//成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
//次序无关
//声明顺序先_a1再_a2
//初始化顺序先_a1再_a2
A(int a)
:_a2(a)//后初始化_a2,此时a是1,所以_a2被初始化为1
, _a1(_a2)//先初始化_a1,此时_a2还是随机值,所以_a1被初始化为随机值
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;//声明顺序先_a1再_a2
int _a2;
};
int main()
{
A x(1);
x.Print();
return 0;
}
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数【也就是可以只允许传入一个参数就能调用的】,还具有类型转换的作用。
class Date
{
public:
//单参数的默认构造函数
Date(int year)
:_year(year)
{
}
//第一个参数没有缺省值,其余参数都有默认值的默认构造函数
//Date(int year, int month = 10, int day = 13)
//{
// _year = year;
// _month = month;
// _day = day;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
//支持隐式类型转换
Date d2 = 2022;
return 0;
}
这有点像之前的两种拷贝构造的形式(但不是,只是样子像):
Date d1(2022);
Date d2(d1);
Date d3=d1;//但是这里是同类型的对象的赋值(拷贝)
其实隐式类型转换我们再引用那一块学过:
不同类型之间的玩法:
Date d1(2022);
//隐式类型转换:小时候的版本:内置类型到内置类型的引用
int i=0;
double d=i//在这中间有一个const类型的临时变量,但这里是拷贝,可以赋值个d
const double& j=i;//但是在这里是引用,临时变量(const)到j不能权限放大,所以要加一个const修饰
//隐式类型转换:长大后的版本:内置类型到自定义类型的引用
Date d2(d1);
Date d3=2022;//在这中间有一个const类型的临时变量,但这里是拷贝,可以赋值个d3
const Date& d4=2022;//但是在这里是引用,临时变量(const)到d4不能权限放大,所以要加一个const修饰
这个隐式类型转换看上去有点别扭,但是在后面你就会发现这个东西好用的很,比如:
更有甚者,可以一气呵成,简介多了:
#include
void push_back(const string& s);
int main()
{
string s1("hello");
push_back(s1);
//隐式类型转换:一气呵成
push_back("hello");
return 0;
}
但是如果我不想让这种隐式类型转换发生,我可以怎么做呐?
那就是在构造函数前加上一个关键字:explicit,也就是
//单参数的默认构造函数
explicit Date(int year)
:_year(year)
{
}
//第一个参数没有缺省值,其余参数都有默认值的默认构造函数
//explicit Date(int year, int month = 10, int day = 13)
//{
// _year = year;
// _month = month;
// _day = day;
//}
加了explicit后看一下报错信息:
前面是C++98支持的单参数构造函数,那么在C++11后,又支持了多参数构造函数。
同样的加一个explicit就不能支持多参数构造了
没学之前如果我们想知道程序总共调用了几次构造函数【默认构造和拷贝构造】,我们可能写的是一个全局变量:
int N = 0;
class A
{
public:
A(int a=0)
:_a(a)
{
++N;
}
A(const A& aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
};
const A& func1(const A& a)
{
return a;
}
A func2(A a)
{
return a;
}
int main()
{
A a1(1);//++N
A a2 = 2;//优化后只有构造函数,++N
A a3 = a1;//++N
cout << N << endl;//3
func1(a1);
cout << N << endl;//验证传引用不要拷贝构造:3
func2(a2); //验证传值传参要拷贝构造:5
cout << N << endl;
return 0;
}
或许是这样写的:
class A
{
public:
A(int a = 10)
:_a(a)
{
++N;
}
A(const A& a)
:_a(a._a)
{
++N;
}
int GetN()
{
return N;
}
private:
int _a;
static int N = 0; //N是局部的
//这里还只是声明,必须有一个地方定义(初始化列表不能定义静态成员变量)
};
int A::N = 0;//N定义
int main()
{
A a;
cout << a.GetN() << endl;//1
return 0;
}
但是C++不是很喜欢写全局变量,理由是在类外面有些人想改N很容易就能改掉,不安全,所以我们也要把封装的思想应用于此处,我们就把N定义在类里面,同时加上static;
一个知识点:加上static修饰后,N变量位于静态区,属于类,但是是每个类共享的。
类型 | 作用域 | 生命周期 |
---|---|---|
局部+static | 局部 | 整个程序 |
全局+static | 全局 | 整个程序 |
类域+static | 类域 | 整个程序 |
接着上面的程序,把变量N封装好了,并提供了GetN()函数供我们使用,似乎一切完美
但是这个GetN()函数只能在通过对象来调用,如果我不想不实例化对象就调用这个GetN()怎么办呐?
那就要用到我们这里要讲的静态成员函数[在函数前面加上static修饰]
作用 :可以通过类名::静态成员函数的方式 直接调用
class A
{
public:
A(int a = 10)
:_a(a)
{
++N;
}
A(const A& a)
:_a(a._a)
{
++N;
}
//静态成员函数
static int GetN()
{
return N;
}
private:
int _a;
static int N;
};
int A::N = 0;
int main()
{
cout << A::GetN() << endl;
A a1;
cout << a1.GetN() << endl;
A a2;
cout << a1.GetN() << endl;
A a3;
cout << a3.GetN() << endl;
return 0;
}
ps:静态成员变量和静态全局变量都是位于静态区的,而非属于每一个栈上的对象的,在语法上,C++就规定不能在构造函数的初始化列表定义静态成员函数
这里大家可以看到我试图在初始化列表定义静态成员变量,但是编译器就直接给我把它掐死了
到这里我想给大家区分一下C++和Java中调用静态成员函数的方式:
C++:类名::函数名
Java:类名.函数名
牛客网用静态巧解题:JZ64 求1+2+3+…+nhttps://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=23248&ru=/exam/oj/ta&qru=/ta/coding-interviews/question-ranking&sourceUrl=%2Fexam%2Foj%2Fta%3Fpage%3D1%26tpId%3D13%26type%3D13
这个题就是拿来秀骚操作的,拿来练练手的:
class Sum {
//调用n次构造函数
public:
Sum()
{
_ret+=_i;
++_i;
}
static int GetSum()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetSum();
}
};
如果我有一个要求:要求对象只能定义在栈上:
(体现封装和静态成员变量的妙用)
//要求定义的对象只能在栈上
class A
{
public:
static A GetObj(int a = 100)
{
A aa(a);
return aa;
}
private:
A(int a = 10)
:_a(a)
{
;
}
private:
int _a;
};
int main()
{
//static A aa1;
//A aa2;
//A* ptr = new A;
//你要调用这个GetObj()你得先创建对象,但是要创建对象必须得先调用GetObj()
A aa4 = A::GetObj();
return 0;
}
之前我们讲过了友元函数,今天我们再来聊一聊友元相关的问题
先来复习一下友元函数:
友元类 :我是你的友元,我就可以偷你的家
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递 如果C是B的友元, B是A的友元,则不能说明C时A的友元。 友元关系不能继承,在继承位置再给大家详细介绍
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;
};
这个内部类又叫做类中类,在C++中不怎么重要,但是在Java中还是很重要的
class A
{
private:
int _a;
public:
class B
{
private:
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;//4
return 0;
}
这相当于两个独立的类,
但是B的访问收到A的类域和域作用限定符的限制
B天生就是A的友元
class A
{
private:
int _a;
static int k;
public:
//如果定义成私有,这个B类就是A专属的类
//这样内部类的话,B天生就是A的友元
class B
{
public:
void Print(const A& a)
{
cout << a._a << endl;
}
private:
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;//4
A aa;
/*
* B bb;//找不到B类型,需指定
*/
A::B bb;
bb.Print(aa);
return 0;
}
用内部类改一改之前那个题:
(把Sum类作为Solution类的内部类)
class Solution {
public:
class Sum
{
public://内部类+Sum_Solution
Sum()
{
//类Sum为类Solution的友元
_ret+=_i;
++_i;
}
};
int Sum_Solution(int n) {
//调用n次构造函数,static修饰记录调用次数,_ret累加
Sum arr[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i=1;
int Solution::_ret=0;
内部类了解一下就可以了,不用深究,C++ 不推荐使用
懒人专用–哈哈哈,看到后面你就知道了
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 aa0;
A aa1(2);
A aa2 = 2;
//A aa3();
//这下面两种方式也在定义对象--
//匿名对象--生命周期只在当前这一行
A();
A(3);
}
匿名对象一般没有生命价值,但是在一些特殊的情形有价值,比如:
拿上面那个牛客求1+2+3…+n的题例子:
class Solution {
public:
class Sum
{
public://内部类+Sum_Solution
Sum()
{
//类Sum为类Solution的友元
_ret += _i;
++_i;
}
};
int Sum_Solution(int n) {
//调用n次构造函数,static修饰记录调用次数,_ret累加
Sum arr[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
int main()
{
//有名对象:要先单独创建对象再调用函数
Solution su1;
su1.Sum_Solution(3);
//匿名对象:直接创建并使用
Solution().Sum_Solution(3);
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "构造函数" << endl;
}
private:
int _a;
};
A func()
{
/*
A ret(10);
return ret;
*/
//匿名对象:名字都懒得起
return A();
}
int main()
{
A aa = func();
return 0;
}
当两个步骤合在一起走的时候,编译器会对一些场景做出优化
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "构造函数" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "拷贝构造" << endl;
}
private:
int _a;
};
void func1(A aa)
{
;
}
A func2()
{
A aa;
return aa;
}
int main()
{
//优化场景1:
A aa1 = 1;//A tmp(1)+A aa1(tmp) -->优化成A aa1(1)
cout << "__________________________________" << endl << endl;
//优化场景2:
/*
A aa2;
func1(aa2);
*/
func1(A(1));//构造+拷贝构造--->优化成构造
func1(1);//构造+拷贝构造--->优化成构造
cout << "__________________________________" << endl << endl;
//优化场景3:
func2();//构造+拷贝构造
A ret = func2();//构造+拷贝构造+拷贝构造---->优化成构造+拷贝构造
return 0;
}
对于优化场景3中:
A ret = func2();//构造+拷贝构造+拷贝构造---->优化成构造+拷贝构造
如果两个步骤分开写,编译器就不敢随便优化,怕你中间还要用到,但是两个步骤合并起来写,就会极致优化.