我们先看下面这段代码
class A
{
private:
int _a;
const int _b;
int& _c;
public:
A(int a,int b,int& c)
{
_a=a;
_b=d;
_c=c;
}
};
这段代码有问题吗?咋一看,确实没什么问题,该赋值的都赋值了,但是我们在仔细看一下这些被赋了初值的成员变量,除了_a,其他两个成员变量按照规定,应该在定义的时候就应该被初始化了,而构造函数的函数体内是赋初值的地方,换句话说,这些变量在进入构造函数体之前就已经开辟好空间了,那么问题是哪里是定义这些成员变量的地方呢?
这里就要引入初始化列表这一概念,专门用来定义成员变量的地方,即所有的成员变量都会先在这里定义开辟空间,这里说明一下,初始化列表无论写不写,编辑器都会走,因为成员变量需要定义开辟空间,不写相当于只定义不给初始值,函数体内只进行赋初值的操作
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式,如下
class A
{
private:
int a;
const int b;
int& c;
public:
A(int a,int b,int& c)
:_a(a)
,_b(b)
,_c(c)
{
//...
}
};
注意:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
1)引用成员变量
2)const成员变量
3)自定义类型成员(且该类没有默认构造函数时),举个例子
class A
{
private:
int _a;
public:
A(int a)//非默认构造函数
{
_a = a;
}
};
class B
{
private:
A a;
int b;
public:
B(int a,int b)
:a(a)//可以调用构造函数
,b(b)
{
//...
}
};
(这里就能解释为什么不写初始化列表,自定义成员只能调用默认构造函数,因为没有给参数,编辑器只能调用默认构造函数)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
次序无关
好,现在我们来看看下面这段代码的结果是什么?
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 1,但是请大家看仔细了,我们先声明的是_a2,然后是_a1,所以初始化列表的顺序是先走_a2(_a1),很显然这时候_a1没有初始化,是随机值,所以_a2是随机值,再走_a1(a),_a1被初始化为1
还有一点补充:其实类的成员变量是可以给缺省值的(如果初始化列表没有写,那么会将成员变量定义为缺省值,注意:先走的初始化列表,然后再走的构造函数,如果一个成员变量在初始化列表和构造函数里都被赋值了,那么它的最终值就是构造函数中被赋的值)
class A
{
public:
A()
{}
private:
int _a=1;
int _b=2;
int _c=3;
};
下面介绍另一种调用构造函数的方法
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year)
:_year(year)
,_month(1)
,_day(1)
{
cout << "Date(int year)" << endl;
}
//拷贝构造是构造函数的函数重载,本质也是构造函数的一种,所以也有初始化列表
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date&d)" << endl;
}
};
int main()
{
Date a(1);//正确
Date b = 1;//正确
return 0;
}
上面代码的两种方法其实都是调用了构造函数,但是其实底层的原理稍有区别,第一种就是单纯的调用构造函数,第二个本质其实是发生了隐式类型转换,先调用构造函数创建了一个临时的Date类型的变量,再拷贝构造给b,但是编辑器将这个过程优化成了直接调用构造函数,如下
那么如果我们不想让构造函数的调用发生类型转换怎么办?这里就要介绍explicit关键字了,用explicit修饰的构造函数就不能用第二种方法构造
explicit Date(int year)
:_year(year)
,_month(1)
,_day(1)
{
cout << "Date(int year)" << endl;
}
当有多个参数的构造函数时,可以用{}调用
Date(int year,int month)
:_year(year)
,_month(month)
,_day(1)
{
cout << "Date(int year)" << endl;
}
Date x={2023,8};
class A
{
private:
int _a;
int _b;
static int _c;
public:
A(int a=1,int b=1)
:_a(a)
,_b(b)
{}
};
int A::_c = 1;
第一个问题:A这个类的大小是多少?
答案是8,因为静态成员变量在静态区,不处于某个单一的对象,而是处于这个类的所有的对象
第二个问题:既然静态成员函数不属于某个单一的对象,那么构造函数能对静态成员初始化吗?
答案是不能,因为构造函数仅仅只能初始化对象的成员变量,而静态成员变量处于所有这个类的对象,如果每个对象创建时都会对静态成员初始化,这是不符合静态变量的定义的,静态变量只能被初始化一次
第三个问题:那么静态成员变量在哪里初始化?
很显然,上面的代码已经给出了答案,静态成员变量在类外初始化
与正常的成员函数不同,静态成员函数没有this指针,也就是说静态成员函数不能访问该对象(即调用这个函数的对象)里面的非静态成员,(但是可以将该对象当作参数传给静态成员函数,这样就能访问非静态成员变量了),当然这样也有一个好处:我们可以用 类名::函数 的方式直接调用静态成员函数
class A
{
private:
int _a;
int _b;
static int _c;
public:
A(int a=1,int b=1)
:_a(a)
,_b(b)
{}
static void Print()
{
cout << _c << endl;//只能访问静态成员变量
}
static void Print(const A&d)//当作参数传进来,可以访问
{
cout << d._a << " " << d._b <<" "<< _c << endl;
}
};
int A::_c = 1;
int main()
{
A a;
a.Print();
a.Print(a);
A::Print();
A::Print(a);
return 0;
}
总结:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
之前我在类和对象(中)介绍了运算符重载,那C++的输入输出其实就是<<和>>两个操作符的重载实现的,那么如果我们要实现一个日期类的输出,怎么写?(先看代码和结果,待会会解释代码)
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;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << '/' << d._month << '/' << d._day << endl;
return out;
}
int main()
{
Date x(2023,8,2);
Date y(2023,8,3);
cout<
注意:cout属于ostream(输出流),cin属于istream(输入流)
函数解释:<<操作符有两个操作数,第一个是输出流,即cout,第二个是要输出的对象,这里就是Date,所以该函数有两个参数,至于这个返回值是为了防止出现连续打印的情况,就如上面的cout<
问个问题:上面的代码有什么可以改进的吗?或者说有什么不足?
其实我们在仔细看看代码就会发现,operator<<函数能在类外要访问成员变量,就意味着成员变量是用public限定符修饰的,但是一般情况下,成员变量是不让在类外访问的,那么怎么办?
有人或说,那把该函数放在类里面变成成员函数不就行了,但这又会有一个小问题,就是成员函数的第一个参数默认是this指针,那么<<操作符的两个操作数的顺序就会改变,变成x<
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;
}
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 x(2023, 8, 2);
Date y(2023, 8, 3);
cout << x << y;
return 0;
}
将<<运算符重载函数声明成日期类的友元函数,就可以访问该类的成员对象,可以在类里面的任意位置声明,只要在前面加上friend关键字就行,大家可以自行实现一下输入操作符重载函数
注意事项:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同
和友元函数很相似的一个概念
1.友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
2.友元关系是单向的,即A是B的友元类,那么A可以访问B,但是B不能访问A
3.友元关系不能传递,即A是B的友元类,B是C的友元类不代表A也是C的友元类
4.友元关系不能继承(这点先了解一下,等后面的继承部分会具体讲)
举例如下
class A
{
friend class B;//注意:这里声明的是B是A的友元类,即B可以访问A中的成员变量
private:
int _a = 1;
int _b = 1;
};
class B
{
private:
int _x;
A _y;
public:
void Print()
{
cout << _y._a << " " << _y._b;//访问私有成员
}
void func(const A& d)//友元函数
{
cout << d._a << " " << d._b;
}
};
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,
它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越
的访问权限。
注意:内部类就是外部类的友元类
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的(受到限定符的制约)
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系
class A
{
class B
{
private:
int _x;
int _y;
public:
B()
:_x(1)
, _y(1)
{}
void func(A x)//友元函数
{
//直接访问外面类的静态成员
cout << _c;
Func();
//
cout<< x._a;
}
};
private:
int _a;
static int _c;
B _b;
public:
static void Func()
{
cout << "hh" << endl;
}
};
class A
{
private:
int _a = 1;
int _b = 1;
public:
A()
{
cout << "A()" << endl;
}
void func()
{
cout << "hh" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A a;
A();//匿名对象的生命周期就只有这一行
cout << "------" << endl;
A().func();//当我们只为了调用类里面的某个函数时,就可以用匿名对象
return 0;
}
当然这匿名对象的使用场景还是很多的,等到后面用到,会再提,这里先给大家认识一下
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。注意:这个优化没有标准规定,不同的编辑器的优化会稍有区别,但大部分满足如下几点
1.隐式类型,连续 构造+拷贝构造=>优化为直接构造
2. 一个表达式中,连续 构造+拷贝构造=>优化为一个构造
3.一个表达式中,连续 拷贝构造+拷贝构造=>优化一个拷贝构造
总结一下:在一个表达式中,有连续构造或者拷贝构造,编辑器就有可能进行优化,具体的还得看情况