目录
1.再探构造函数——初始化方式
1.1 构造函数体内赋初值
创建对象时,在调用的函数体内为对象的成员变量赋初值,见下面代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
但是函数体内的赋值语句只能将其称作赋初值,在此之前内置类型已有初始值(随机值),不能称为初始化。初始化只能有一次,而在构造函数体内可以多次赋值。
1.2 初始化列表
初始化列表初始化,以一个冒号开始,接着用逗号分隔的数据成员列表,每个成员变量后面跟着一个放在括号中的初始值或表达式,见下面代码:
class Date
{
public:
Date(int year, int month, int day): _year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
[注意]
- 每个成员变量在初始化列表中只能出现一次(初始化只有一次),初始化列表就是定义成员变量的阶段(开辟空间)。
- 初始化列表和1.1节所讲的函数体内初始化可以并用,不会冲突,但是有些成员变量必须使用初始化列表,这些成员变量如下所示:
- 引用成员变量
- const成员变量
- 没有默认构造函数的自定义类型成员
引用类型和const修饰的变量在定义时是必须初始化的,而初始化列表就是定义的阶段(初始化列表开辟内存空间),所以必要出现在初始化列表中。
自定义类型的成员变量,如果没有默认构造函数,且没有在初始化列表中调用构造函数,那么在定义时没有可调用的构造函数,那就无法成功定义该自定义类型成员。
以上三种成员变量的共性是:必须在定义的时候初始化。
反之,其他类型的变量由于没有要求定义的时候初始化,于是可以放在任意位置(函数体内,初始化列表或其他能访问的位置)进行初始化。
class A
{
public:
//非默认构造函数
A(int a)
{
cout << "A的构造" << endl;
_a = a;
}
private:
int _a;
};
class Date
{
public:
Date(int year=0, int month=1, int day=1) :_n(10),_ref(year),_aobj(10)//初始化列表初始化
{
//_n = 10;//_n不是可修改的左值
//_ref = 10;//引用类型必须列表初始化
}
private:
int _year;
int _month;
int _day;
//注意这三种变量:引用,const常量,自定义 都需要在定义时初始化,因此需要备足条件
const int _n;
int& _ref;//定义引用变量是必须要初始化的,在类里只是声明,在实例化时才是定义了这些变量(开辟空间)
A _aobj;//无默认构造函数的自定义类型
};
- 尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会使用初始化列表初始化。当然前提是该自定义类型有默认构造函数,否则会报错
- 成员变量在初始化列表中初始化的顺序就是成员变量在类中声明的次序,与其在初始化列表中的先后次序无关
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();
}
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
类中声明的次序是先声明 _a2
,后声明 a1
,在初始化时用_a1的随机值为_a2初始化,应选D
1.3 explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数或者有n个参数,但有n-1参数提供默认值的情况,还具有类型转换的效果。
class Date
{
public:
Date(int year):_year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2020);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用int值2021构造一个无名的临时Date对象,最后用该临时对象给d1对象进行赋值
d1 = 2021;//int类型为Date类赋值,隐式的类型转换
//如果要建立引用,则需添加const,因为引用的是隐式转换的临时对象(右值属性)
const Date& d2=2022;
}
使用 explicit
关键字修饰构造函数可以禁止单参的构造函数的隐式转换。
但是可以选择显式的类型转换:
d1=(Date)2021
d1=Date(2021)
这种实际上是先构造匿名对象,再赋值给d1。
1.4 匿名对象
class A
{
public:
A(int a=0) :_a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void f()
{
cout<<"匿名变量的调用" << endl;
cout<<d._a<<endl;
}
private:
int _a;
};
int main()
{
A aa1(1); //标准的构造函数调用,定义有名对象
A aa2 = 2; //隐式类型转换,编译器优化后直接调用构造
A(3); //构造匿名对象,生命周期只在这一行 ,这一行结束该变量就会被析构
//什么场景需要匿名对象
//1.匿名变量的好处就在于可以调用一些无参的成员函数,目的只在突破类域调用这些函数,
//调用完后匿名即可销毁,但如果函数需要匿名对象的值,则匿名对象当做参数引用时必须加const;
A(2).f();//类名A后面的括号中要不要带值,根据构造函数而定,默认的构造函数则无需传参
//2.一些函数需要一些对象作为左值传参,而这些对象是谁并不重要,我们只要它的值。
//直接定义有名对象反而麻烦,直接定义匿名对象,方便快捷,且即用即销毁不浪费空间。
vector<A> v;
v.push_back(A(1));//匿名对象
v.push_back(a1);//有名对象
v.push_back(3);//隐式的类型转换,将会先构造临时对象,如果构造函数explicit,则不能使用,因为不能隐式转换
return 0;
}
2. static 成员
2.1 概念
声明为static的类成员,我们称之为类的静态成员,用static修饰的成员变量,称为静态成员变量。
用static修饰的成员函数我们称之为静态成员函数。
静态成员变量一定要在类外进行初始化。
面试题:实现一个类,计算中程序中创建出了多少个类对象。
class A
{
public:
//所有的对象要么是构造出来的
A()
{
++_scount;
}
//要么是拷贝构造出来的
A(const A& t)
{
++_scount;
}
//静态成员函数
//与普通成员函数的区别在于没有this指针,不能访问非静态成员,也不能访问非静态函数
//只能访问静态成员(因为静态成员属于整个类,不需要this指针)
static int GetACount() { return _scount;}
private:
static int _scount;
};
//静态成员变量在类外的定义初始化
int A::_count = 0;
int main()
{
cout<<A::GetACount()<<endl;
A a1, a2;
A a3(a1);
cout<<A::GetACount()<<endl;
}
2.2 使用特性
静态成员的作用域在类内,生命周期是全局的。
-
静态成员为所有类对象共享,不属于某个具体的实例对象
-
静态成员变量必须在类外定义,定义时不需要添加static对象,在定义时需要添加作用域限定符
::
-
公有类静态成员的访问(突破类域):a)类名
::
静态成员,b)对象.静态成员 -
静态成员没有隐藏的this指针,在静态静态成员函数中不能访问静态成员变量,也不能调用静态成员函数。
反之,非静态成员函数是可以访问静态成员变量和调用静态函数的,因为突破了类域。
-
静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值
3. C++11的初始化
C++11可以为类内的非静态成员提供声明的缺省值,这并非初始化,而是在定义对象时为成员变量提供一个初始化的值,具体见下面代码:
在调用默认构造函数的情况下为对象 aa
的成员附上了声明时的缺省值。
定义对象时成员变量初始值的优先级:
注意类内初始值不能用圆括号赋值:
class A
{
private:
int _a(10);//会被当成函数声明
};
会被当成是函数声明。
如果成员为自定义类型,可以使用{缺省值},实现默认的声明缺省值。
class A
{
public:
A(int a1=0,int a2=0):_a1(a1),_a2(a2)
private:
int _a1;
int _a2;
};
class B
{
public:
private:
int _b=0;
A _a={1,1};
};
4.友元
友元的引入:
我们的屋子不会让陌生人进来参观,除非他是我们的朋友——友元。
友元分为:友元函数 和 友元类。
友元提供了突破了封装的方式,有时提供了便利,但是友元同时也破坏了封装,增加了“家庭成员”被篡改的风险。
4.1 友元函数
一个类A的成员只有类内成员可以访问以及调用,如果一个类外的函数需要访问类A内的成员,那这个函数首先需要成为类A的朋友——友元函数。
通过友元函数,类外的函数可以访问类中的所有成员。
友元函数的声明格式:
friend + 函数声明
友元函数的使用方法如下:
class INTEGER
{
friend void Print(const INTEGER& obj);//声明友元函数
};
void Print(const INTEGER& obj)
{
//函数体
}
void main()
{
INTEGER obj;
Print(obj);//直接调用
}
4.1.1 全局函数作为友元函数
问题:尝试在类内重载operator<<
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1) :_year(year), _month(month), _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1 << cout;//颠倒<<的使用习惯
return 0;
}
我们发现在类内重载operator<<函数,会使 <<
运算符使用颠倒,因为在类内this指针将默认为成员函数的第一个参数,this指针因此成为了 <<
的左操作数。在实际使用中cout应作为左操作数,于是我们需要将operator<<重载成全局函数,又因为我们要输出对象中的成员变量,需访问类内成员,于是这里需要用友元处理:
class Date
{
//全局函数的友元声明
friend ostream& operator<<(ostream&, Date&);
public:
Date(int year = 2020, int month = 1, int day = 1) :_year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//在全局函数中重载
ostream& operator<<(ostream& _cout,Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
int main()
{
Date d1;
cout<<d1;
return 0;
}
结果:
重载 operator>>
也是同理。
前方高能:
万一当类外函数声明在类前时 ostream& operator<<(ostream& _cout,Date& d);
,程序此时还不知道有Date这个类,那我们就需要前向声明类 class Date;
- 类的前向声明:他向程序中引入了这个名字Date,并且指明Date是一个类类型。对于Date来说,在它的声明之后和定义之前是一个不完全类型,即我们知道了Date是一个类,但是不知道其中包含哪些成员。
于是需要作为友元函数的类外的函数也不能马上定义,因为程序不知道Date类的细节,它不认识Date成员变量
//类的前向声明
class Date;
//类外函数声明
ostream& operator<<(ostream& _cout,Date& d);
//不能定义函数,因为此时程序还不知道Date的细节,不认识 _year _month _day,
//类的定义
class Date
{
//全局函数的友元声明
friend ostream& operator<<(ostream&, Date&);
public:
Date(int year = 2020, int month = 1, int day = 1) :_year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
}
//此时程序知道了Date的细节,最后进行类外友元函数的定义
ostream& operator<<(ostream& _cout,Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
一般情况下会先定义类,再定义其友元函数。
大多数时候是定义和声明分离——.h,.cpp,然后把类本身和友元的声明(类的外部)放在头文件中。因此,我们的Date头文件应该为operator<<提供独立的声明(除了类内部的友元声明之外)。
4.1.2 成员函数作为友元函数
一个类的成员函数可以作为另一个类的友元函数,这个知识点我们将在友元类中讲解
4.1.3 友元函数的说明:
- 友元函数不能用const修饰,(因为没有this指针)
- 友元函数不受访问限定符(public,private,protected)的限制,可以在类定义的任何地方声明(最好在类定义开始或结束前的的位置集中声明友元)
- 一个函数可以作为多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
4.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的所有成员(包括私有成员和保护成员)。当希望一个类存储和读取另一个类的私有成员时,可以将该类声明为另一个类的友元类。
4.2.1 友元类举例
我们将Date类作为Time类的友元类:
class Time
{
friend class Date;
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 = 2020, int month = 1, int day = 1) :_year(year), _month(month), _day(day)
{}
//Date是Time的友元类,Date的函数可以访问Time的所有成员
void SetTime(int hour, int minute, int second)
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
void PrintTime(const Time& t)
{
cout << t._hour << ':' << t._minute << ':' << t._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
- 说明:
-
友元关系是单向的,Date在Time中声明为友元,那么说明Date是Time的友元,Date可以访问Time中的所有成员,但是反过来,Time并不是Date的友元,Time也无法访问Date中的任何成员。
-
友元函数并不具备传递性,如果类B是类A的友元,且类C是类B的友元,那么不能推出类C是类A的友元,C不能理所当然的具有访问A的特权。
每个类负责控制自己的友元类或者友元函数。
4.2.2 成员函数作为友元函数
除了令整个Date类作为Time类的友元之外,Time还可以只为Date的某些成员函数提供访问权限。
当把一个成员函数声明为友元函数时,我们必须明确指出该成员函数来自于哪一个类:
class Time
{
//Date::PrintTime 必须在Time类之前被声明
friend Date::PrintTime(const Time& t);
//Time类的剩余部分
//...
};
-
前方高能
Date类中的成员函数以Time类作为参数,Time类中又有Date类的成员函数作为友元函数,两个类彼此交错,
要想令某个成员函数作为友元函数,我们必须组织好程序的结构,以满足声明和定义彼此的依赖关系,在此例中,我们必须按照如下顺序设计程序:
- 先声明Time类,告诉程序有Time这个类类型。随后定义Date类,在其中声明PrintTime函数,但是不能定义它,因为程序不知道Time类的成员细节。
- 接下来定义Time类,包括PrintTime函数的友元声明。
- 最后定义PrintTime函数,此时他才可以使用Time成员。
class Time;//声明Time,跟程序打好招呼
class Date
{
public :
void PrintTime(const Time& t);//程序知道Time这个类,但是不知道其细节,故不能定义
};
class Time//Time的定义
{
friend void Date::PrintTime(const Time& t);//友元函数声明
private:
int _hour=12;
int _minute=30;
int _second=15;
};
void Date::PrintTime(const Time& t)//在对Time知根知底后,定义该友元函数
{
cout << t._hour << ':' << t._minute << ':' << t._second << endl;
}
int main()
{
Date d1;
Time t1;
d1.PrintTime(t1);
}
结果:
更一般的讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。——不能无中生友
5. 内部类
5.1 概念
如果一个类A定义在另一个类B的内部,那么A称为B的内部类。
内部类是一个独立的类,并不属于外部类。
但是内部类是外部类的友元类,内部类具有访问外部类的成员的权限。外部类不是内部类的友元类,外部类的对象没有访问内部类的权限。
- 特性:
-
内部类可以定义在外部类的public,protected,private中任何一个位置
如果内部类定义在public,则可通过 外部类名::内部类名 来定义内部类的对象。
如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。
-
内部类可以直接访问外部类的static成员,而不需要通过特定的对象访问
-
外部类的大小只是其自身的成员变量的大小,不包含内部类的大小。
-
class A
{
public:
A(int a=0,int* p=nullptr):_a(a),_p(p)
{
}
//内部类
class B
{
public:
B(int b = 10) :_b(b)
{
cout << "B()" << endl;
}
void funcB(const A& a)
{
//cout << _a << endl;//非静态成员必须要通过特定对象访问
//这是一个非常常见的错误。因为内部类是一个独立的类,不属于外部类,此时还没有外部类的对象,显然也不存在_a。
cout << a._a << endl;
cout << n << endl;//友元已突破类域,无需通过类名或者对象来访问静态变量
// 而n是静态成员,不需要外部类的对象就已存在,所以这里B作为A的友元类直接访问n是OK的。
}
private:
int _b;
};
private:
int _a = 0;
static int n;
};
int A::n = 10;
int main()
{
A aa;
A::B bb;
bb.funcB(aa);
return 0;
}
青山不改 绿水长流