目录
再谈构造函数
构造函数体赋值
初始化列表
explicit关键字
static成员
友元
友元函数
友元类
内部类
再次理解封装
再谈构造函数
首先要明白声明、定义、初始化三个概念的不同。
声明:指定变量的名字和类型,可以多次声明。
定义:为该成员变量分配存储空间,有且仅有一个定义。
初始化:为该成员变量赋初值。
在类的声明中,静态成员变量仅完成了声明过程,并没有进行定义和赋初值。
静态成员变量在编译时存储在静态存储区,即定义过程应该在编译时完成,因此一定要在类外进行定义,但可以不初始化。
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表语法格式:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
需要注意每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化劣币位置初始化:
- 引用成员变量
- const成员变量
- 子应用类型成员(且该成员类没有默认构造函数时)
class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:B(int a, int ref):_aobj(a),_ref(ref),_n(10){}
private:A _aobj; // 没有默认构造函数int& _ref; // 引用const int _n; // const
};
尽量使用初始化劣币初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先试用初始化劣币初始化。
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);
}
成员变量在类中声明次序就是其在初始化劣币中的初始化顺序,与其初始化列表中的先后次序无关。
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();
}
explicit关键字
class Date
{public:Date(int year) :_year(year){cout<<"Date(int year)" << endl;}Date(const Date& date){cout << "Date(const Date& date)" << endl;}
private:int _year; //声明};
int main()
{Date d(2022);Date d2 = 2022; //隐式类型转换}
这段代码就是隐式类型转换,因为 i 和 j 相近类型,意义相似,他们都是表示数据大小,所以可以将 i 赋值给 j ,隐式类型转换还会产生一个临时变量,而下面这段代码发生的就是强制类型转换
int* p = &j; int d = (int)p;
因为他俩一个表示地址,一个表示大小,不发生隐式类型转换,但是可通过强制类型转换。
而此刻这个日期类也发生了隐式类型转换,本来整形和日期类没有任何关系,但是我们写的类支持一个单参数的构造函数后,整形就可以构造一个日期类的对象。
本来是用2022构造一个临时对象Date(2022),再用这个对象拷贝构造d2,但是C++编译器在连续的一个过程中,多个构造会被优化,合二为一,所以这里被优化为直接构造。注意:虽然他俩都是直接构造但是他俩的意义是不一样的。
我们可以用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
class Date
{
public://不想让这个过程发生就加这个关键字explicit Date(int year) :_year(year){cout << "Date(int year)" << endl;}Date(const Date& date){cout << "Date(const Date& date)" << endl;}
private:int _year; //声明};
static成员
static静态的。此时申请的内存区域在数据段(静态区)。生命周期也就可程序一致。
静态成员:
如果是静态成员,那么,在初始化列表无法进行初始化,只能在类外进行初始化。但是由于C++语法特性,在static前加上const即就可以直接在类里进行初始化了,就无需在类外进行。静态成员属于此类,该类的所有对象均可以使用。
静态函数:
在成员函数前加上static就是静态函数。此时就不会默认给this指针了。既然没有this指针就无法访问成员变量,只能访问静态成员。而且此时静态函数也可以不用通过对象去调用,直接前面加上类域即可
实现一个类,计算程序中创建出了多少个类对象,如果想要字节数会很难数,因为编译器会员优化的场景,所以通过程序解决
int _count = 0;
class A
{
public:A(int a = 0){_count++;}A(const A& a){_count++;}private:int _a;
};void f(A a)
{
}int main()
{A a1;A a2 = 1;f(a1);cout << _count << endl;
}
这是已经实现好的,但是会有很多缺点,_count是全局变量,会被随意修改,
如果我们将他定义为成员变量呢? 也是不行的,因为这样的话,_count就成了每个对象都有的成员起不到计数的作用。
class A
{
public:A(int a = 0){_Scount++;}A(const A& a){_Scount++;}private:int _a;//静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间static int _Scount; //声明
};
int A::_Scount = 0; //定义初始化
在成员变量前加static就可以把他定义成静态成员变量了。静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间。在类型外面定义的时候,要指定它是属于这个类。
友元
友元函数
正所谓friend,即友好关系,适用于函数和类。
友元函数:
此函数可以访问此类中的所有成员。
声明方式:在类中使用friend加在此函数前进行声明即可。
首先,我们来填上上面在用流提取流插入重载的坑,因为需要cin、cout作为第一个参数,所以需要定义成全局函数,但是为了能够访问类中的成员,所以此时在日期类里声明为友元函数即可:
class Date
{//一般友元关系声明在第一行:friend inline ostream& operator<<(ostream& out, const Date& d);friend inline istream& operator>>(istream& in, Date& d);//......
}
//声明为内联 -- 节省栈帧
inline ostream& operator<<(ostream& out, const Date& d)
{out << d._year << '/' << d._month << '/' << d._day << endl;return out;
}inline istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
友元类
友元类:
此时这个类可以访问声明友元的类里的所有成员。但是注意友元关系是单向的,即声明友元的类就不可访问对应的这类里面的私有、保护成员。
class Test1
{friend class Test2;//友元类int _num;
public:Test1(int num = 0):_num(num){}
};
class Test2
{Test1 t;
public:void Print(){cout << t._num << endl;}
};int main()
{Test2 t;t.Print();return 0;
}
此处友元类就可以访问声明友元的类的所有成员了,当然,友元是单向的关系,不可以反过来进行访问。
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的 类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
- 特性:
- 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;//OKcout << a.h << endl;//OK}};
};int A::k = 1;int main()
{A::B b;b.foo(A());return 0;
}
再次理解封装
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了
在类和对象阶段,一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象