文章目录
初始化列表
初始化列表的引入
再谈构造函数
在创建对象的时候,编译器通过调用构造函数,给对象的每一个成员变量一个初始值。那么它是什么时候给它的初始值呢?
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 hour)
{
_year = year;
Time t(hour);//先构造
_t = t;
}
private:
int _year;
int _hour;
Time _t;
};
class Time
{
public:
//如果没有默认构造函数
Time(int hour)
{
_hour = hour;
}
private:
int _hour;
};
上面的例子中,Time
没有默认构造函数,在调用Date
的构造函数的时候,
由于是在函数体内赋初始值,所以对于自定义类型Time
,需要先构造出一个对象
然后在进行拷贝构造来给Time
类型的成员变量赋初值.
显然十分麻烦
所以C++设计了一种初始化的方式初始化列表:
初始化列表定义和使用
初始化列表是在函数体的前面,以一个冒号开始,接着是一个以逗号分隔 的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
C++规定,在初始化列表的时候对成员变量进行初始化
内置类型:如果不给显示初始化,默认是随机值(不做处理)自定义类型:如果不给显示初始化,调用自定义类型的默认构造函数
如果给了显示初始化,那么就按照显式初始化的值
所以由该规定可知,上面的代码会出错!
因为Time没有默认成员函数,会出现找不到默认成员函数的错误
也就是说,上面的Date的构造函数在编译器的视角其实是这样的:
Date(int year, int hour)
:_t() //此处自动调用_t类型的默认构造函数
{
_year = year;
Time t(hour);//先构造
_t = t;
}
运行结果:
此时显示Time没有默认构造函数可用
是因为,在Date()之后有一个初始化列表,这里进行一次对象的初始化
- 对于内置类型如果没显示给初始化,初始化为随机值
- 对于自定义类型是调用自定义类型的默认构造去实现初始化
而Time没有默认构造,所以就会报错
之前所说的创建一个对象就会调用构造函数去初始化对象,初始化就是在这个地方进行初始化的
如果提供了默认构造,就在函数体直接对自定义类型进行初始化:
可以看到,在进入函数体之前就对自定义类型进行了初始化(调用默认构造函数)
所以,利用初始化列表,不用在构造函数函数体内进行初始化了
在函数体之外就可以完成
- 对于内置类型,在初始化列表和函数体内部进行初始化实际上差不多
- 但对于自定义类型的成员变量,在初始化列表初始化是高效直接的,当然如果该自定义类型没有默认构造函数,必须显示使用初始化列表
/* 如果Time没有默认构造:*/*
Time(int hour)
{
_hour=hour;
}
//此时要初始化_t成员,只能通过初始化列表
Date(int year,int hour)
:_t(hour)
{
_year=year;
}
/**********************************************************/
/* 如果Time有默认构造 */
Time(int hour = 0)
{
_hour=hour;
}
//初始化_t成员,可以在函数体内赋值,但是还是会先走初始化列表调用默认构造
Date(int year,int hour)
{
_year=year;
Time t(hour);
_t = t;
}
/** 既然如何都要调用初始化列表,最好的方式:自定义类型直接使用初始化列表
(不管自定义类型有没有默认构造,都不会出错)**/
Date(int year,int hour) Date(int year,int hour)
:_t(hour) :_t(hour)
{ 或 ,_year(year)
_year = year; {}
}
总结 1
自定义类型成员推荐使用初始化列表初始化,内置类型成员无所谓
初始化列表可以认为是成员变量定义的地方
必须在初始化列表初始化的三种成员
除了无默认构造函数的自定义类型成员之外,还有两种类型的成员变量必须在初始化列表进行初始化
- 引用成员变量
- const成员变量
原因:因为const修饰的变量具有常性,只有一次初始化的机会
如果在初始化列表的位置不进行初始化,那么之后这个变量就一直是随机值了,不可以再改了,所以C++规定 对于const修饰的变量,必须在初始化列表的位置进行初始化
对于引用成员变量同理,引用只能在定义的时候初始化
int& ra = a
并且如果成员变量是引用类型,那么构造函数的传参对应就是一个引用
否则无法修改外部实参的值(此时一定是需要传递一个变量来修改的)
如:
class A
{
public:
//这里形参需要是引用,_c才能是main中实参的别名
A(int a,int b,int& c)
:_a(a)
,_b(b)
,_c(c)
{
_c++;//实参x也++了
}
private:
int _a;
int _b;
int& _c;
}
int main()
{
int x = 0;
A aa(1,1,x);
return 0;
}
所以,三种必须在初始化列表进行初始化的成员:
- 引用成员变量
- const成员变量
- 自定义类型成员(并且没有默认构造)
初始化列表的其他注意事项
1. 内置成员的缺省值
如果给内置成员变量一个缺省值,那么该缺省值是什么时候赋值给成员变量的呢?
没错,就是在初始化列表
在初始化列表没有显示给值的时候,缺省值就发挥作用了
所以,对于上面的必须在初始化列表进行初始化的成员,也可以在声明的时候给这些成员一个缺省值,这样在走初始化列表的时候,可以不用显式初始化,它会自动拿对应的缺省值去初始化!
如果不给缺省值,就必须显式初始化了
2. 不适合使用初始化列表的情景
如果我们需要动态开辟内存,那么初始化列表就不是很合适了
class A
{
public:
//初始化一个数组,用初始化列表
A(int N)
:_a((int*)malloc(sizeof(int)*N))
,_N(N)
{
if(_a==NULL)
{
perror("malloc fail");
}
memset(_a,0,sizeof(int)*N);
}
/**************************************************************/
//不使用初始化列表
A(int N)
{
_a = (int*)malloc(sizeof(int)*N);
if(_a==NULL)
{
perror("malloc fail");
}
memset(_a,0,sizeof(int)*N);
}
private:
int* _a;//数组
int _N;
}
所以说,这种情况用初始化列表就显得比较别扭了。
考虑到需要检查,开辟空间,还是在函数体内初始化比较好
3. 初始化列表的初始化顺序
初始化列表的初始化顺序,是根据成员变量的声明顺序来的,谁先声明的谁就先初始化
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print(){
cout<<_a1<<" "<<_a2<<endl;
}
private:
//成员变量的声明
int _a2;
int _a1;
}
// 程序结果:
// 1 随机值
// 因为_a2先声明,_a2先初始化 , _a1后初始化
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
private:
int _year;
};
int main()
{
Date d1(2022);//直接调用构造
Date d2 = 2022;//存在隐式类型转换:构造 + 拷贝构造 +编译器优化
// ->相当于直接调用构造
return 0;
}
对于只有一个参数的构造函数,或者只有第一个参数没有默认值的拷贝构造函数来说,其定义方式有两种
- 直接调用构造函数
- 可以利用赋值
=
来进行构造,这个过程中存在隐式类型的转换。以上面的代码为例:- 首先会利用所给的值(2022)构造一个临时对象
- 然后把构造出来的临时对象拷贝构造给 d2
- 一般编译器会进行优化,直接优化成 直接调用构造
而explicit
关键字就可以阻止这种隐式类型的转换
当在Date前面加了explicit
之后,就变成了这样:
而临时变量具有常性,构造出来的临时对象也有常属性
所以如果利用一个引用接受,必须加const
并且:如果用引用接收,引用的必须是隐式类型转换过程中产生的临时对象
因此用引用接收,编译器不会优化为 直接构造
int main()
{
Date& d1 = 2022;// 编译报错
const Date& d2 = 2022;// 编译成功
return 0;
}
隐式类型转换的应用场景
如果有string类型,传参的时候利用隐式类型转换就会十分自然:
#include<string>
//string存在这样一个构造函数:
/* string(const char* str)
{} */
//自定义类型传参一般给引用
//引用要加const(防止临时对象赋值导致权限扩大
void func(const string& str)
{
/**/
}
int main()
{
string s1("hello");//构造string对象 方式1
string s2 = "hello";//构造string对象 方式2 (隐式类型转换)
func(s1);//调用方式1:传递已经创建好的对象
func("goodboy");//调用方式2:利用隐式类型转换
//(会产生临时对象,形参要用const)
}
匿名对象
匿名对象就是没有定义名字的对象,C++允许这种做法
特点: 匿名对象的生命周期只在定义的这一行
Date(2022);//匿名对象,生命周期只在这一行
//这一行调用构造函数之后马上调用析构函数
匿名对象的用处
如果要调用某一个成员函数,但是只是为了调用函数而没有必要创建一个有名的对象,就可以利用匿名对象
class A
{
public:
void func()
{}
}
int main()
{
A().func();//直接利用匿名对象创建函数
return 0;
}
static
static定义
引入
有些时候需要变量在所有的变量中都用到,比如有一个类person
存在对于个体的名字、年龄
也存在对于整体类的人类历史的成员变量
人类历史是一个对于整个类的变量,而不是对于一个对象
这个成员变量,就适合利用static来修饰
class Person {
public:
void showInfo()
{
/***/
}
private:
char _name[20];
int _age;
static int _peoHistory;
}
概念
声明为static的类成员称为类的静态成员
用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数
静态成员变量一定要在类外进行初始化,因为类中只是声明
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。生命周期是整个程序的运行期间
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用
类名::静态成员
或者对象.静态成员
来访问
这里的对象.
并不是去对象中找,而是突破类域 - 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
这里要注意:
如果成员变量都是公开的public
普通成员函数可以访问普通成员变量 和 静态成员变量
静态成员函数只能访问静态成员变量,不可以访问普通成员变量
如果成员变量是私有的private
普通成员变量的访问需要借助 普通成员函数接口
静态成员变量的访问需要借助 普通成员函数接口或者静态成员函数接口
以下为例子:
class Person
{
public:
void showInfo(){}
static void PrintHistory(){}
private:
int _age;
char _name[20];
public:
static int peoHistory;//静态成员变量
};
//初始化静态成员变量
int main()
{
//定义一个人
Person p1;
p1.showInfo();//调用普通成员函数
p1.PrintHistory();//调用静态成员函数 方式1
Person::PrintHistory();//调用静态成员函数 方式2
cout << p1.peoHistory << endl;//通过对象访问静态成员变量
cout << Person::peoHistory << endl;//通过类作用限定符访问静态成员变量
return 0;
}
static使用场景
场景1
实现一个类,计算程序中创建出了多少个类对象
这个场景下就需要一个不依赖于对象的变量来记录对象的个数
所以就需要static
成员变量
分析:
类的创建无非两个方法:
- 构造方法
- 拷贝构造
类的销毁就是 析构方法
所以只要调用构造方法或拷贝构造,对象数量+1
而调用析构方法,对象数量-1
class A
{
public:
//构造
A() { ++_scount; }
//拷贝构造
A(const A& t) { ++_scount; }
//析构
~A() { --_scount; }
static int GetACount()
{
return _scount;
}
private:
static int _scount;
};
//在类外面初始化
int A::_scount = 0;
int main()
{
//输出类对象的数量
cout << A::GetACount() << endl;
}
场景2:定义一个只能在栈上定义对象的类
定义一个对象可以在栈上、堆上、静态区上
如果要求你定义一个类,这个类的对象只能定义在栈上,如何实现呢?
因为定义对象一定会调用构造函数,所以如果构造函数是开放的(可以让我们随便用的)
那么该对象就可以定义在栈上、堆上、静态区都可以
但是如果把构造函数设置为私有,而只提供一个创建对象的接口函数(静态的)
因为静态函数不依赖于对象就可以调用,而函数一定会开辟栈帧,这样在函数中创建的对象一定就是在栈上定义的对象,然后对象作为返回值来返回。
class StackOnly
{
public:
//提供静态函数接口,来创建对象
static StackOnly CreateObj()
{
StackOnly so;
return so;//作为返回值的对象,一定是定义在栈上的
}
//把构造函数设置为私有
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
//利用接口来创建的对象,一定在栈上
StackOnly so = StackOnly::CreateObj();
return 0;
}
友元
有时候我们在类的外面无法访问类成员,通过友元就突破这种封装,有时候会提供遍历
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
之前遇到的,如果想要重载operator <<
,也就是cout运算符
因为在类中定义的成员函数第一个参数一定是this(默认的),我们无法更改
但是要实现的cout,cout才是第一个参数才符合使用逻辑:
cout << x
即x流入cout,如果是x << cout
显然用起来很别扭,不符合常规调用
所以operator<<
函数必须在全局实现,不可以在类中
但是在全局又存在一个弊端,我们无法访问到类成员变量
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
所以,这里利用友元可以解决
class Date
{
//声明该函数是这个类的友元函数(理解成朋友)
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//在类外面定义 << 的重载
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
Date d;
cout<<d<<endl;//此时cout就是第一个参数,d就是第二个参数
}
注意事项
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰,因为库中的cout是ostream&类型,我们重载的cout的返回值不可以是const ostream&类型。简单来说就是参数就会不匹配
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数,只需要在不同的类中都进行声明即可
- 友元函数的调用与普通函数的调用原理相同
友元类
如果类A中要经常访问类B中的成员,那么直接可以把类A设置成类B的友元类
(对B来说,A是B的朋友,可以随便访问)
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
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 = 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;
Time _t;
};
注意事项
友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
友元关系不能继承(后面解释)。
内部类
把一个类定义在另一类的内部就是内部类。
内部类是一个是一个独立的类,不要认为定义在外部类的内部它就属于外部类。
所以不能通过外部类的对象去访问内部类的成员。
内部类和外部类就相当于两个不同的类
有区别的就是:
- 内部类的访问收到了外部类的限制,需要访问限定符
- 外部类默认是内部类的友元类(友元是单向,内部类不是外部类的友元)
/* Inner类定义在Outer的内部
Inner是Outer的友元 */
class Outer
{
private:
int _x;
static int _z;
public:
//内部类
//内部类是外部类的友元
class Inner
{
public:
void PrintOuter(const Outer& a)
{
//内部类是外部类的友元,可访问外部类的私有成员
cout << a._x << endl//普通成员用对象访问
cout << _z << endl;//静态成员可以直接访问
}
private:
int _y;
};
};
int main()
{
cout << sizeof(Outer) << endl;
Outer obj1;
Outer::Inner obj2;//定义inner类型
return 0;
}
特性
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。