类和对象(下)
1.再谈构造函数
1.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
// 初始化列表
# include<iostream>
using namespace std;class Date
{
public:// 构造函数Date(int year = 1, int month = 1, int day = 1){// 函数体内赋值_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
1.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个**"成员变量"后面跟一个放在括号中的初始值或表达式。**
我们来看代码:
// 初始化列表
# include<iostream>
using namespace std;class Date
{
public: 构造函数//Date(int year = 1, int month = 1, int day = 1)//{// // 函数体内赋值// _year = year;// _month = month;// _day = day;//}// 初始化列表Date(int year = 1, int month = 1, int day = 1):_year(year) // :号 开始, 成员变量用逗号隔开, 每个成员变量后面跟着初始化的值 ,_month(month)// 注意不能使用 _month = month 这是语法规则,_day(day){}private:int _year;int _month;int _day;
};
那有人可能会觉得,那这个初始化列表和我们的构造函数好像没什么区别啊,最终目的都是将成员变量赋值。
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
// 初始化列表的应用场景
# include<iostream>
using namespace std;class A
{
public:A(int a):_a(a){}
private:int _a;
};class B
{
public:B(int a = 10, int ref = 10)// 初始化列表是对象的成员变量定义的地方// 而这三种成员变量 都必须在定义的时候就要初始化.:_aobj(a) // 如果A有默认构造函数,那就可以不用放在初始化列表初始化, _ref(ref) , _n(10){_x = 10; // 可以放在这里,也可以放到初始化列表中}
private:// 这三种成员变量 都必须在初始化列表初始化// 他们都有个共同点,就是在定义的时候就要初始化。A _aobj; // A类没有默认构造函数int& _ref; // 引用const int _n; // const int _x; // 这种成员变量就不需要在定义的时候初始化,因此可以不放在初始化列表中初始化
};int main()
{B b; // 调用默认构造函数return 0;
}
在调试过程中,我们也可以发现,先执行初始化列表,在执行函数体内的赋值。
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
# include<iostream>
using namespace std;class Time
{
public:Time(int hour = 0):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};class Date
{
public:Date(int day)//:_t; {// 如果实在想在函数体内初始化自定义类型,就要这样搞//Time t(1);// _t = t;//但是何必呢,即便这样也要去创建一个变量,也会去调用初始化列表}private:int _day;Time _t; // 对于自定义类型,会自动调用它的初始化列表完成初始化
};int main()
{Date d(1);
}
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
//成员变量在类中的声明次序就是其在初始化列表中的 初始化顺序 ,与其在初始化列表中的先后次序无关
# include<iostream>
using namespace std;class A
{
public:A(int a):_a1(a) // 后初始化_a1 a是1 _a1 就是 1, _a2(_a1) // _a2先初始化,但是_a1是随机值{}void Print() {cout << _a1 << " " << _a2 << endl;}private:int _a2; // _a2先声明 初始化它int _a1;
};int main()
{A aa(1);aa.Print(); // 1 -858993460
}//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值// 最终选 D
总结:
在构造函数这边,尽量使用初始化列表。
如图所示:
如果我们要定义一个对象d1,我们首先要调用它的构造函数去定义它,但是d1这个对象是有成员变量的,你要定义d1 肯定要定义成员变量,此时d1体内只有成员变量的声明,因此我们调用了构造函数去定义其成员变量。
为什么说要尽量使用初始化列表去定义成员变量呢?
并不是说在构造函数体内不能定义成员变量,而是有三种成员变量只能在初始化列表定义。既然都要通过初始化列表去定义其中几个的成员变量,将全部的成员变量放到初始化列表定义也未尝不可。
如图所示:
1.3explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
// explicit关键字
// 单参数的隐式类型转换
# include<iostream>
using namespace std;class Date
{
public:explicit Date(int year):_year(year){}private:int _year;int _month;int _day;
};int main()
{Date d1(2024); // 构造对象Date d2 = d1; // 拷贝构造对象Date d3 = 1; // 隐式类型的转换// 首先会创建一个Date的临时变量, 构造出一个临时对象tmp(1),然后在将这个对象拷贝构造d3(tmp) + 有可能优化成直接构造// 其实之前我们也接触过隐式类型的转换int i = 1;double d = i; //首先创建一个double类型的临时变量 去接受1 ,将1 转化成double类型,再赋值给d// 如何证明呢?//double& d = i; // 编译不通过,因为i会转化成double类的临时变量,临时变量具有常性,只读不能改,这样权限放大了。const double& d = i; // 编译通过 权限不在放大// 因此对于我们的Date d3 = 1; 也是一样的//Date& d3 = 1; // 编译不通过,1会先构造成Date类的临时对象,具有常性,涉及权限放大const Date& d3 = 1; // 编译通过, 引用的就是中间产生的临时对象// 如果构造函数 加了 explicit关键字的话 这里就无法进行隐式类型转换了return 0;
}
多参数的也可以禁用隐式转换,但是c++11才支持:
// explicit关键字
// 多参数的隐式类型转换 (c++ 11)
# include<iostream>
using namespace std;class Date
{
public:explicit Date(int year, int month, int day):_year(year){}Date(const Date& d){}
private:int _year;int _month;int _day;
};int main()
{Date d1 = 1; // c++98// c++ 11 才支持//Date d2 = (1, 2, 3); // 这样是错误的语法形式,是不可以的,花括号才可以Date d2 = { 1,1,1 }; // 隐式类型转换// 如果构造函数 加了 explicit关键字的话 这里就无法进行隐式类型转换了Date d3 = {1,2}; // c++11return 0;
}
总的来说:用explicit修饰构造函数,将会禁止构造函数的隐式转换。
2.static成员
2.1概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
面试题:实现一个类,计算程序中创建出了多少个类对象。
// static成员变量 (静态成员变量)
// 设计出一个类A,可以计算这个类总计生产了多少个对象
// 思路就是,每次创建对象都要调用构造函数,那么通过构造函数去统计生产了多少个对象就行
# include<iostream>
using namespace std;class A
{
public:A(){++_n;}A(const A& a) // 拷贝构造{++_n;cout << GetN() << endl; // 非静态成员函数可以调用静态成员函数// 突破类域+ 访问限定符 就可以访问静态成员函数// 类中是一个整体都在类域中 并且类中不受访问限定符的限制,因此类中可以直接调用静态成员函数}//int GetN()//{// return _n;//}//const int& GetN()//{// return _n;//}static int GetN() // 没有this指针形参,函数中不能访问非静态的成员函数{//_a = 10; // 不允许,不能访问非静态成员return _n;}private:int _a;static int _n; // 这只是个声明, _n位于静态区// _n不是属于某个对象的,是属于类的所有对象的,是属于这个类的
};int A::_n = 0; // 定义A f(A a)
{return a;
}int main()
{A a1; // 1次A a2;// 1次f(a1); // 生成2次对象f(a2);// 2次//a1._n = 10; //外界无法修改,因为我们的_n是私有的成员// 外界也无法访问我们的_n私有属性//cout << A::_n << endl; //cout << a1._n << endl;// 我们可以在类中提供接口给外界,让外界能访问_ncout << a1.GetN() << endl;cout << a2.GetN() << endl;// 外界能访问,但是外界是不能修改的。//a1.GetN() = 10; // 修改无法成功,因为返回的不是_n本身,而是一个临时变量// 除非我们的接口返回值 是 int& 引用// 如果我们非要使用引用提升效率,也可以加个const修饰 这样也无法修改// 但是实际上这里没必要给引用,本来我们就不想外界修改我们的_n// 如果接口的返回值是 static int n cout << A::GetN() << endl; // 此时的接口不属于单个对象,属于整个类cout << a1.GetN() << endl;cout << a2.GetN() << endl;return 0;
}
经过调试可以发现我们的代码没有问题
2.2特性
-
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
-
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
-
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
-
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
-
静态成员也是类的成员,受public、protected、private 访问限定符的限制
【问题】
- 静态成员函数可以调用非静态成员函数吗? 【没有this指针,不可以】
静态成员函数也不能访问非静态成员变量。
- 非静态成员函数可以调用类的静态成员函数吗?【可以】
- 突破 (类域 + 访问限定符) 就可以访问静态成员函数
类中是一个整体都在类域中 并且类中不受访问限定符的限制,因此类中可以直接调用静态成员函数
3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
3.1友元函数
我们先看看友元的用法:
其实就是在类外面我们定义了一个函数,其中需要访问类中的成员变量。
我们通过友元来打破类域,实现成员变量的访问
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
// 友元、
#include<iostream>
using namespace std;class Date
{//友元函数需要在类中声明friend void f(Date& d); // 友元函数
public:Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// c++11 才支持 声明的时候给一个缺省值int _year = 2024; // 这里不是定义,是缺省值,如果构造函数没有定义这个变量,那就调用缺省值int _month = 6;int _day = 2;
};// 正常来说如果我们想在类外边访问类中私有的成员变量,是做不到的
// 但是如果有友元的存在就可以
void f(Date& d)
{d._year = 2024;cout << d._year << endl;
}int main()
{Date d1;d1.Print();f(d1);return 0;
}
其实上述场景友元并不是必须的,我们在类中在定义一个成员函数也可以解决
我们来看一个必须用到友元的场景:
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
注意:
- cout是标准库std中ostream类型的一个全局对象
// 其实上述场景友元并不是必须的,我们在类中在定义一个成员函数也可以解决
// 我们来看一个必须用到友元的场景
// cout 是 ostream 类型
#include<iostream>
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){}ostream& operator<<(ostream& out) // this指针形参无法修改顺序{out << _year << "/" << _month << "/" << _day << endl;return out;}
private:// c++11 才支持 声明的时候给一个缺省值int _year = 2024; // 这里不是定义,是缺省值,如果构造函数没有定义这个变量,那就调用缺省值int _month = 6;int _day = 2;
};ostream& operator<<(ostream& out, const Date& d)
{//通过友元来实现对类内成员变量的访问out << d._year << "/" << d._month << "/" << d._day << endl;return out;
}int main()
{Date d1;Date d2(2024, 6, 2);// 我们知道 涉及到自定义类的运算符都要重载//cout << d1; // 我们发现,即便重载了 输出运算符 仍然编译报错// 因为这里会编译成 cout.operator<<(d1) 而我们期望的是d1.operator<<(cout)d1 << cout; // 1/1/1 这里就可以输出了,但是这个格式不是我们期望的// 但是由于this指针形参是隐含的,因此我们无法修改其顺序// 这个时候友元就出场了cout << d1;// 1/1/1 cout << d1 << d2; // 有连等的使用场景 返回值需要是ostream类型的//1/1/1//2024/6/2cout << d1 << "->" << endl << d2;//1/1/1//->//2024/6/2return 0;
}
学会了cout<<的重载 那么cin>>的重载也就一样的道理。
直接来看代码:
// 学会了cout << d1 的重载 那么 cin >> d1 的重载也是一样的道理
// cin 是 istream 类型的一个全局变量
#include<iostream>
using namespace std;class Date
{friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& out, Date& d);public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}private:// c++11 才支持 声明的时候给一个缺省值int _year = 2024; int _month = 6;int _day = 2;
};ostream& operator<<(ostream& out, const Date& d)
{//通过友元来实现对类内成员变量的访问out << d._year << "/" << d._month << "/" << d._day << endl;return out;
}int GetMonthDay(int year, int month)
{// 给13 恰好让下标对上 月份int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 判断是否是闰年if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)day[2] = 29;return day[month];
}istream& operator>>(istream& in, Date& d)
{int year, month, day;cout << "请输入年份" << endl;in >> year;if (year > 0) // 判断输入的年是否合法d._year = year;else{cout << "非法年份的输入" << endl;return in;}cout << "请输入月份" << endl;in >> month;if (month > 0 && month < 13) // 判断输入的月是否合法d._month = month;else{cout << "非法月份的输入" << endl;return in;}cout << "请输入日" << endl;in >> day;if (day > 0 && day < GetMonthDay(d._year, d._month)) // 判断日是否合法d._day = day;else{cout << "非法日的输入" << endl;return in;}return in;
}int main()
{Date d1, d2;cin >> d1 >> d2; // cin.operator>>(cin, d1)cout << d1 << d2;return 0;
}
总结:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰,因为没有this指针的概念了
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
注意:
为什么char和int之类的内置类型 可以直接支持不用我们重载呢?
因为库里已经帮我们实现好了
3.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如下面的代码: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;
};// 我们的Date类需要经常访问Time类的成员
// 如果一个一个都搞成友元函数有点麻烦
// 这个时候友元类就派上用场了
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){_t = 1;}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;
};
4.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
-
内部类可以定义在外部类的public、protected、private都是可以的。
-
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
-
sizeof(外部类)=外部类,和内部类没有任何关系。
来看代码:
// 内部类
# include<iostream>
using namespace std;class A
{
private:static int k;int h;
public:class B // B天生就是A的友元, B可以访问A的友元{public:void foo(const A& a){cout << k << endl;//OK // k是静态成员变量,直接访问即可,无需cout << a.h << endl;//OK // h需要通过A类的对象来访问}private:int _a;};void f(const B& b){//cout << b._a << endl; // A不是B的友元,A无法访问B类的成员}
};
int A::k = 1;int main()
{A::B b; // 要声明B类是属于A类的才能找到B类b.foo(A());return 0;
}
5.匿名对象
// 匿名对象
# include<iostream>
using namespace std;
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}private:int _a;
};class Solution
{
public:int Sum_Solution(int n) {//...return n;}
};int main()
{A aa1;//A aa1();// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义A();// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数A aa2(2);// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说Solution().Sum_Solution(10); // Solution() 是属于Solution类的一个匿名对象return 0;
}
总结:
- 匿名对象不需要名字,直接就是 类名(),代表这个类的一个对象
- 匿名对象的生命周期只有一行,下一行就会调用析构函数
- 匿名对象针对一些特定场景是很好用的
可以看到代码的执行结果也能验证我们的说法
6.拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
// 拷贝对象时编译器会做的一些优化
# include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(const A & aa):_a(aa._a){cout << "A(const A& aa)" << endl;}A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a = aa._a;}return *this;}~A(){cout << "~A()" << endl;}private:int _a;
};void f1(A aa)
{}A f2()
{A aa;return aa; // 会创建一个临时变量接受aa 并返回的是临时变量
}int main()
{// 传值传参A aa1;f1(aa1);cout << endl;// 这段代码的运行结果//A(int a) // aa1调用的构造函数//A(const A & aa) // aa1传进f1 调用了拷贝构造//~A() // aa形参由于函数栈帧结束调用了析构函数// 传值返回 // 【其实传值返回应该调用一次构造函数,拷贝构造函数和析构函数,但是编译器做了优化】f2();cout << endl;//A(int a) //~A()// 隐式类型,连续 构造+拷贝构造->优化为直接构造f1(1); // 本来隐式类型的转换,需要先构造一个临时变量接受1并构造,然后再拷贝构造给形参aacout << endl;//A(int a)//~A()// 一个表达式中,连续 构造+拷贝构造->优化为一个构造f1(A(2)); // 先构造A(2) 在拷贝构造给形参aacout << endl;//A(int a)//~A()// 一个表达式中,连续 拷贝构造+拷贝构造->优化为一个拷贝构造A aa2 = f2();cout << endl;//A(int a)// 一个表达式中,连续 拷贝构造+赋值重载->无法优化aa1 = f2();cout << endl;//A(int a)//A& operator=(const A & aa)//~A()//最后调用两次析构函数,先把aa2析构,再把aa1析构。 【后进先出】//~A()//~A()return 0;
}
上述代码的运行结果如图所示:
如果对返回值的优化有问题的话, 详情可以看
C++中临时对象及返回值优化 - Mr.Rico - 博客园
7.再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
-
用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
-
经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
-
经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
-
用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象
7.1再次理解封装
我们都说面向对象有三大特征,封装、继承和多态
我们再来理解一下封装,封装其实就是为了让我们去更好的管理类,让跟类有关联得全部放到类里面,并且如果不想给外面看,就设置成私有,想给外面看就设置成公有。并且要尽量降低和其他类之间的联系。这一过程叫做低耦合、高内聚
封装好了之后。我们就更好的维护和管理我们的类。
8.练习题
来看一段代码的构造顺序和析构顺序
在来看一个选择题:
如果编译器不优化的话,一共应该调用9次拷贝构造。但是编译器总是会在传值和返回值处优化
因此实际上是7次拷贝构造。
编程题:
-
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
-
计算日期到天数转换_
-
日期差值
-
打印日期_
-
日期累加_