目录
构造函数补充
构造函数体赋值
初始化列表
explicit关键字
Static成员
概念
特性
友元
友元函数
友元类
内部类
匿名对象
编译器对拷贝对象的优化
理解类和对象
构造函数补充
构造函数体赋值
构造函数:
我们知道 构造函数本质就是在对象创建的同时对其进行初始化操作!
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初值!
代码:
#include <stdio.h> using namespace std; //日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 17); return 0; }
虽然上述构造函数调用后,对象已经有了一个初始值,但是该初始值不能称之为对象中成员变量的初始化,而是赋值,在构造函数体中的语句,就是进行赋值操作,而不是初始化,因为初始化只能初始化一次,而构造函数体内可以多次赋值。而定义并初始化的部分是在构造函数初始化列表部分来完成的!
初始化列表
初始化列表:
初始化列表是构造函数的一部分,我们知道在类中书写的成员变量是声明,只有在对象实例化的时候才会定义开空间,而对象在实例化的时候是调用构造函数的,所以可以认为初始化列表是成员变量定义的位置!
写法:
构造函数下面紧接着写:以一个冒号开始,接着是一个以逗号隔开的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式!
代码:
#include <stdio.h> using namespace std; //日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) :_year(year), _month(month), _day(day) {} private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 17); return 0; }
注意:
1、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2、类中包含以下成员,必须放在初始化列表位置进行初始化:
1>引用成员变量
2>const成员变量
3>自定义类型(且该类没有默认构造函数时)
代码:
//因为引用在定义的时候必须要初始化 //const修饰的成员是不能被改变的,只能在初始化的时候给值 //而对于自定义的类型,会去调用它的默认构造函数 //若没有默认构造,就得需要将其写入初始化列表进行初始化 #include <iostream> using namespace std; class Basu { public: Basu(int a) :_a(a) {} private: int _a; }; class Definitely { public: Definitely(int a, int ref) :_aobj(a) , _ref(ref) , _n(10) {} private: Basu _aobj; // 没有默认构造函数的类 int& _ref; // 引用 成员 const int _n; // const 成员 }; int main() { return 0; }
3、尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员,都会先使用初始化列表初始化!
代码:
//不管是否使用初始化列表初始化 //对于自定义类型都会去走初始化列表进行初始化 //对于自定义类型会默认去调用它的默认构造 //不管是否书写在初始化列表初始化 //都会去走初始化列表 #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) {} private: int _day; Time _t; }; int main() { Date d(1); }
4、成员变量在类声明的次序,就是它在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关!
代码:
//初始化列表中是按照类中声明的顺序进行初始化的 #include <iostream> using namespace std; //该类中是先声明a2,再定义a1 //初始化列表在初始化的时候先初始化a2,再初始化a1 //先初始化a2 a2是用a1初始化的,因为a1没有初始化,a1的值是随机值 //所以a2的值是一个随机值 //再用 a初始化a1 ,a我们传值为1,所以a1的结果为1 class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); }
总结:
初始化列表是成员变量定义的地方,对于类中的成员变量,我们在初始化列表位置不管是否显示写,默认都要走初始化列表,但对于三种情况必须要在初始化列表显式书写,(1、引用成员。2、const修饰的成员,3、没有默认构造函数的自定义类型!)原因:引用在定义的时候必须初始化。const成员定义的时候需要给值,因为只能在定义的时候初始化一次,后续就不能改变了,所以必须走初始化列表。对于没有默认构造函数的自定义类型而言,我们知道构造函数对于自定义类型会去调用它的默认构造,若是没有默认构造,该自定义类型就无法完成初始化操作,此时就需要我们去处初始化列表指定调用它的初始化函数,或者对它进行初始化!
代码:
//默认构造函数分为三种: // 1、全缺省的默认构造函数 // 2、无参的默认构造函数 // 3、不写编译器默认生成的默认构造函数(编译器默认生成的是一个无参的构造函数) // 初始化列表是成员定义的地方 // 引用成员必须显式写在初始化列表中 // const 成员必须显式写在初始化列表中 // 无默认构造的自定义类型成员,也必须显式写在初始化列表中 #include <iostream> using namespace std; //时间类 class Time { public: //构造函数(非默认构造函数) Time(int hours,int minutes,int second) :_hours(hours), _minutes(minutes), _seconds(second) {} //打印 void Print() { cout << _hours << " : " << _minutes << " : " << _seconds << endl; } private: int _hours; int _minutes; int _seconds; }; //日期类 class Date { public: //默认构造函数 Date(int year = 0, int month = 0, int day = 0,int a=20) :_year(year), _month(month), _day(day), _t(16,22,36),//无默认构造的自定义类型 _a(a),//引用成员 _b(20)//const成员 {} //打印 void Print() { cout << _year << " - " << _month << " - " << _day <<" "; _t.Print(); } private: int _year; int _month; int _day; Time _t;//自定义类型 int& _a;//引用成员 const int _b;//const 成员 }; int main() { Date d1(2023, 5, 17); d1.Print(); return 0; }
explicit关键字
类型转换:
对于构造函数不仅可以构造和初始化对象,还对于,只有一个参数或者第一个参数没有默认值,其余参数都有默认值的构造函数,具有隐式类型转换的作用!
例如:
#include <iostream> using namespace std; class Date { public: //单参构造函数,没有使用explicit修饰,具有类型转换作用 Date(int year) :_year(year) {} Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1(2022); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值 //有隐式转换,而无名对象是临时生成的对象,具有常属性! d1 = 2023; return 0; }
#include <iostream> using namespace std; class Date { public: /* 虽然有多个参数,但是创建对象时后两个参数可以不传递, 没有使用explicit修饰,具有类型转换作用*/ Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; void Test() { Date d1(2022); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值 d1 = 2023; } int main() { Test(); return 0; }
隐式类型转换:
隐式类型转换会生成一个临时对象,而临时对象具有常属性!
总:
上述两份代码 分别是单参构造和第一个参数没有默认值的构造函数,当构造函数是上述两者的时候,对于对象进行操作是具有隐式类型转换作用的!
解决方案:
若不想让其具有隐式类型转换,则只需要将两种构造函数用explicit 关键字修饰即可!
代码:
#include <iostream> using namespace std; class Date { public: // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用 // explicit修饰构造函数,禁止类型转换 explicit Date(int year) :_year(year) {} /* 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递, 没有使用explicit修饰,具有类型转换作用 explicit修饰构造函数,禁止类型转换 explicit Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} */ Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1(2022); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值 d1 = 2023; // 因为explicit修饰构造函数,禁止了单参构造函数类型转换所以会报错 return 0; }
总结:
单参以及第一个参数没有缺省值的构造函数,在对象操作时会产生隐式类型转换,也就是产生临时对象,临时对象具有常性,而在新的编译器下会优化一些操作,比如会将一行中的构造+拷贝构造优化成构造……等!若不想让其发生隐式类型转换,用explicit来修饰单参或者第一个参数无缺省值的构造函数即可!修饰之后若有隐式转换的场景编译器会报错!
Static成员
概念
概念:
在类中用static修饰的成员,称为类的静态成员,,用static修饰的成员变量,称为静态成员变量。用static修饰的成员函数,称为静态成员函数。(注:静态成员变量一定在类外面进行初始化)
代码:
#include <iostream> using namespace std; class Date { public: //构造 Date(int year = 0, int month = 0, int day = 0) :_year(year), _month(month), _day(day) {} //静态成员函数(不存在对象中,是在静态区中) static int GetMonthDay(int year,int month) { int MonDay[13] = { 0,31,29,31,30,31,30,31,31,30,31,30,31 }; if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { return 29; } return MonDay[month]; } private: int _year; int _month; int _day; static int _a;//static成员静态成员 //是不走初始化列表 //需要在类外面定义 }; //在类外面定义 int Date::_a = 10; int main() { Date d1; return 0; }
特性
静态成员的特性:
1、静态成员函数为所有类对象共享,不属于某个具体的对象,存在静态区
2、静态成员变量必须在类外面定义,定义时无需添加static关键字,但要指定类域,类中只是声明。
3、静态成员可以用(类名::静态成员)或者(对象.静态成员)来访问
4、静态成员函数没有隐藏的this指针
5、静态成员也是类的成员,也受类访问限定符的限制(public、private、protected)!
代码:
//1、静态成员函数为所有类对象共享,不属于某个具体的对象,存在静态区 //2、静态成员变量必须在类外面定义,定义时无需添加static关键字,但要指定类域,类中只是声明。 //3、静态成员可以用(类名::静态成员)或者(对象.静态成员)来访问 //4、静态成员函数没有隐藏的this指针 //5、静态成员也是类的成员,也受类访问限定符的限制(public、private、protected)! #include <iostream> using namespace std; class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) :_year(year), _month(month), _day(day) {} //静态成员函数 //静态成员函数没有this指针 static int GetMonthDay(int year, int month) { 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)) { return 29; } return Day[month]; } private: int _year; int _month; int _day; static int _a;//静态成员变量 }; //静态成员变量必须在类外面定义, //定义时无需添加static关键字,但要指定类域,类中只是声明。 int Date::_a = 20; //静态成员函数为所有类对象共享,不属于某个具体的对象,存在静态区 int main() { //静态成员可以用(类名::静态成员)来访问 //cout << Date::_a << endl;//无法访问静态成员变量_a //因为静态成员是受类访问限定符的限制的 int day = Date::GetMonthDay(2023, 5); cout << day << endl; //(对象.静态成员)来访问 Date d1; int dayy = d1.GetMonthDay(2024, 5); cout << dayy << endl; return 0; }
友元
友元关键字: firend
概念:
类的友元,突破了类的封装性,可在类外面访问类的私有或保护成员,类的友元需要在类中声明,声明时用friend修饰!
简述:
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。
分类:
友元分为:友元函数 和 友元类
友元函数
问题:
当我们在日期类中去重载operator<<,发现没办法将 operator<< 重载为成员函数,因为cout输出流对象将对象的this指针位置抢了,本来对象是要传递给隐含的this指针的,但是由于在书写的时候cout要写在前面,导致cout传给了this指针,而对象传给了另一个参数,这样结果就紊乱了。所以要将operator<<重载成全局函数,,而重载成全局函数,之后,没法通过该函数去访问类的成员,此时就需要友元来修饰该函数,来解决问题!
代码:
//重载 << 运算符要重载到全局位置 //重载到类中会发生this指针传递问题 #include <iostream> using namespace std; class Date { public: //构造 Date(int year=0,int month=0,int day=0) :_year(year), _month(month), _day(day) {} 重载 << 运算符 若重载到类中会发生this指针传递问题 d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用 // // 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧 //ostream& operator<<(ostream& out) //{ // out << _year << "年" << _month << "月" << _day << "日"; //} //为了在类外面访问类中的成员用友元来修饰该函数 //全局函数声明的时候需要加上friend修饰 //说明该函数是该类的友元 //可以通过该全局函数,访问类的私有或保护成员! friend ostream& operator<<(ostream& out, const Date& d); private: int _year; int _month; int _day; }; //重载到全局位置 ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } int main() { Date d1(2023, 5, 21); cout << d1; return 0; }
注意:
1、友元函数可以访问类的私有成员和保护成员,但其不是类的成员函数
2、友元函数不能用const修饰,因为没有this指针
3、友元函数可以在类中任何地方声明,不受类访问限定符的限制
4、一个函数可以是多个类的友元函数
5、友元函数的调用和普通函数的调用原理相同
代码:
#include <iostream> using namespace std; //1、友元函数可以访问类的私有成员和保护成员,但其不是类的成员函数 //2、友元函数不能用const修饰 //3、友元函数可以在类中任何地方声明,不受类访问限定符的限制 //4、一个函数可以是多个类的友元函数 //5、友元函数的调用和普通函数的调用原理相同 class Date1 { public: Date1(int year=0,int month=0,int day=0) :_year(year), _month(month), _day(day) {} //打印函数 friend void Print(int year, int month, int day); private: int _year; int _month; int _day; }; class Date { public: //构造函数 Date(int year=0,int month=0,int day=0) :_year(year), _month(month), _day(day) {} //友元函数可以在类中任何地方声明,不受类访问限定符的限制 //打印函数 friend void Print(int year, int month, int day); private: int _year; int _month; int _day; }; //友元函数不能用const修饰 //const修饰的是this指针,而友元函数是全局函数 //没有this指针所以不能用const修饰 //一个函数可以是多个类的友元函数 //打印函数 void Print(int year,int month,int day) { cout <<year << "-" << month << "-" << day << endl; } int main() { //友元函数的调用和普通函数的调用原理相同 Date d1(2023, 5, 22); Print(2023, 5, 22); return 0; }
总结:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
友元类
概念:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问一个类中的非公有成员。
注意:
1、友元关系是单向的,不具有交换性
例如下述代码 Date 类 和 Time类,在Time类中声明Date类为其友元,
那么可以在Date类中直接访问Time类的私有成员,
但想在Time类中访问Date类中的私有成员是不被允许的!
2、友元关系不能传递
如果C类是B类的友元,B类是A类的友元,是不能说明C类是A类的友元的!
3、友元关系是不能继承的(后续文章会写继承)
代码:
#include <iostream> using namespace std; 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 = 2000, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SelectTime(int hour, int minute, int second) { // 直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } void Print() { cout << _year << "-" << _month << "-" << _day<<" "; cout << _t._hour << ":" << _t._minute << ":" << _t._second << endl; } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1(2023, 5, 22); d1.SelectTime(9,37,52); d1.Print(); return 0; }
总结:
友元类不具有传递性、友元关系不具有交换性、友元关系不能继承!
内部类
概念:
如果一个类定义在另一个类的内部,这个类就是内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限!
代码:
#include <iostream> using namespace std; class Time { public: //构造 Time(int hours=0,int minutes=0,int seconds=0) :_hours(hours), _minutes(minutes), _seconds(seconds) {} //内部类 class Date { public: //构造 Date(int year=0,int month=0,int day=0) :_year(year), _month(month), _day(day) {} private: int _year; int _month; int _day; }; private: int _hours; int _minutes; int _seconds; }; int main() { Time t1(9, 53, 46); //不能通过外部类对象去访问内部类成员 //cout << t1._year << endl; return 0; }
注意:
内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但外部类不是内部类的友元。不能通过外部类访问内部类成员!
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象或类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。
4. 用内部类实例化对象,不能直接使用内部类去实例化,需要在类名前面指定外部类 类域
代码:
#include <iostream> using std::cout; using std::endl; class A { private: static int k; int h; public: class B // B天生就是A的友元 { public: void foo(const A& a) { //访问外部类的static成员 cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { //通过类域指定用创建内部类对象 A::B b; b.foo(A()); //sizeof(外部类) 与内部类无关,不包含内部类! cout << sizeof(A) << endl; return 0; }
总结:
内部类天生就是外部类的友元,不能通过外部类访问内部类的成员,内部类可以定义在外部类内任意位置,sizeof(外部类) 跟内部类没有任何关系,且内部类可以直接访问外部类的静态成员。在定义内部类对象时,需要用外部类进行域作用限定符指定!
匿名对象
概念:
在C++中存在一种没有名字的对象,该对象被称之为匿名对象。匿名对象是直接用类去创建。匿名对象的特性是:即用即销毁!
特点:
匿名对象即用即销毁,在当前行定义,当前行结束就销毁了。
代码:
#include <iostream> using namespace std; class Date { public: //默认构造 Date(int year=0,int month=0,int day=0) :_year(year), _month(month), _day(day) { cout << "Date(int year=0,int month=0,int day=0)" << endl; } //析构 ~Date() { cout<<"~Date()"<< endl; } private: int _year; int _month; int _day; }; int main() { //有名对象 Date d1(2023, 5, 22); //匿名对象 //即用即销毁,只在当前行有效 Date(2023, 5, 5); Date(); return 0; }
注意:
1、匿名对象在创建的同时调用类的成员!
2、用const 引用匿名对象,延长了匿名对象的生命周期!
匿名对象实际上是通过默认构造创建出一个临时对象,而临时对象具有常性,
用const引用是可行的!
代码:
#include <iostream> using namespace std; class Date { public: //默认构造 Date(int year = 0, int month = 0, int day = 0) :_year(year), _month(month), _day(day) { //cout << "Date(int year=0,int month=0,int day=0)" << endl; } void Print()const { cout << _year << "年" << _month << "月" << _day << "日" << endl; } //析构 ~Date() { //cout << "~Date()" << endl; } private: int _year; int _month; int _day; }; int main() { //创建的同时调用成员 Date(2023, 5, 22).Print(); //用const引用之后当前行结束匿名对象并没有销毁 // 可认为const延长了匿名对象的生命周期 //用const引用匿名对象 const Date& d1 = Date(); d1.Print(); return 0; }
总结:
匿名对象即用即销毁,用const引用延长了匿名对象的生命周期,可在定义的同时通过匿名对象调用类的成员!
编译器对拷贝对象的优化
概念:
对象在传参或传返回值的过程中,都有可能会形成多种拷贝,一般新的C++编译器都会对其进行优化处理,减少对象拷贝次数,以提高效率!
#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; } 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. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清 楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、 Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣 机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才 能理解洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
类是对某一类实体(对象)来进行描述的,描述该对象具有那 些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化 具体的对象。