前言
我们前几期已经对C++的类和对象的一些基础知识点进行的较为系统的介绍,我们本期将继续对C++的类和对象的知识进行进一步的介绍!
本期内容介绍
再谈构造函数
static成员
友元
内部类
匿名函数
拷贝对象时的一些编译器的优化
一、再谈构造函数
在创建对象时,编译器会自动调用构造函数,给对象中各个成员变量一个合适的初始值!例如我们以前的日期类:
cpp
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
**虽然调完之后对象的成员变量(属性)就已经有了一个初始值!但不能将其称为是对对象中的成员属性初始化(定义)!构造函数体中的语句只能将其称为,为对象的成员属性赋值。而不能称作初始化,因为初始化只能初始化(即定义)一次,而构造函数体内可以多次赋值。**我们上期介绍过,成员属性在类那里是声明!那他在哪里定义呢?这就是我们将要介绍的初始化列表!
初始化列表
在介绍初始化列表前,你肯定有这样的疑问以前没有什么初始化列表我照样可以给成员变量一个合适的初始值!现在又来个初始化列表有啥用呢?其实我一开始学习这块的时候也有这样的疑惑的!下面我将用一个例子来证明初始化列表的意义!
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
p = year;
c = day;
}
private:
int _year;
int _month;
int _day;
int& p;
const int c;
};
int main()
{
Date d;
return 0;
}
这就是上面的日期类稍微改造了一下!此时这里就会有问题!首先我们在C++入门的那一期的时候介绍过,没有空引用和野引用的说法!引用在申明时必须初始化即引用一个实体!!所以这里类的申明会有问题。因为const修饰的常变量也是如此他只能在初始化的时候给一次值,后续不可修改! 所以我们以前玩的在函数体内给初始值的做法已经行不通,如何解决呢?就是初始化列表!
什么是初始化列表?
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每一个"成员变量"后面跟一个放在括号中的初始值或表达式。
上述的Date类可以修改成如下:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)//初始化列表初始化
, _month(month)
, _day(day)
, p(year)
, c(1)
{}
private:
int _year;
int _month;
int _day;
int& p;
const int c;
};
int main()
{
Date d;
return 0;
}
此时各个变量都有了自己定义(初始化的)地方!
初始化列表的注意点
1、每个成员变量在初始化类表中只能出现一次(初始化直能初始化一次)
这个是很好理解的!初始化只能初始化一次但赋值可以赋值多次!而初始化列表就是每个成员的定义的地方即只能出现一次,构造函数的函数体内是赋初始值的地方可以出现多次赋值!
2、类中包含 引用成员变量、const成员变量、自定义类型的成员变量(该类没有的默认构造函数),必须在初识列表中进行初始化!
上面刚刚介绍了两种,引用成员变量和const成员变量,如果不在初始化列表初始化在构造函数的函数体内是无法进行给一个初始值的!这两种就不在多哔哔了!下面我来用一个例子说明第三个!
用我们在数据结构中介绍过的那用两个栈实现一个队列为例!
cpp
class Stack
{
public:
Stack(int n)
{
_a = (int*)malloc(sizeof(n * sizeof(int)));
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
_size = 0;
_capacity = n;
}
private:
int* _a;
int _size;
int _capacity;
};
class MyQueue
{
public:
private:
Stack _push;
Stack _pop;
int _top;
};
此时的MyQueue类的成员属性有三个,两个自定义类型(Stack)和一个内置类型。我们前面一期说过如果我们不写构造函数的话,内置类型会被初始化(定义)为随机值,自定义类型定义完后去调用它的默认构造进行赋值,但这里它的默认构造是没有的,所以无法进行初始化!此时就必须要用初始化列表对其进行初始化的!
这里我又想到了一个问题:对于内置类型定义时给的随机值这一点C++11打了一个补丁即可以在声明的时候给一个缺省值!既然他是声明那缺省值是给谁的?其实它就是给初始化列表的!!
cpp
class MyQueue
{
public:
MyQueue(int n = 3)
: _push(n)
, _pop(n)
, _top(0)
{
}
private:
Stack _push;
Stack _pop;
int _top;
};
此时,MyQueue类给了构造,会去走MyQueue的构造,自定义类型就会用给定的值去调用Stack的构造函数,从而给MyQueue的成员变量初始化!
这里有个问题就是初始化列表这么牛逼那我们是不是全部都是用初始化列表而舍弃函数体的初始化呢?
**先说答案:肯定不行!有些地方是必须用函数体的!**一般函数体和初始化列表混着用即可!
原因是尽管初始化列表可以做很多事情但有一些还是不好处理的!例如检查和后续的继续操作!
3、建议尽量使用初始化列表初始化,因为不管你是否用初始化列表,对于自定义类型而言一定是先使用初始化列表初始化的!
1、你写了构造函数但没有写初始化列表,一样会走初始化列表定义,自定义类型去调他们自己的默认构造,内置类型不做处理!(注意必须自定义成员的那个类必须要有默认构造!!!)
2、你没写构造函数,一样会走初始化列表定义和上面的情况一样!
但是如果你想自己控制具体的参数的话或者自定义类型的成员变量没有提供默认构造函数的话,就得自己必须在初始化列表显示的定义!!!!!
如下:
3、成员变量在类中的声明次序就是初始化列表的声明顺序,与在初始化列表的先后顺序无关
看一个非常经典的笔试题:
cpp
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();
return 0;
}
如上代码的结果是啥?1 1 还是啥?我们来看结果!
原因就是上面的初始化列表的顺序就是成员变量声明的顺序!这里_a2先声明所以先走此时_a1定义了但里面是随机值,所以_a2是随机值,然后a = 1给_a1赋值为1,结果为1 和 随机值!
ecplicit关键字
构造函数不仅可以为对象的成员属性进行初始化,对于单参数或者可接受第一个参数的构造函数具有类型转换的作用!
例如下面的场景:
cpp
class A
{
public:
A(int a)
{
_a = a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
正常情况下我们得调用构造函数来实例化一个对象:
由于这里支持单参数的构造,我们可以这样玩:
这里是用一个常量先是去构造了一个A类型的对象,即我们以前的临时中间对象。然后用这个中间对象去拷贝构造了aa2!
我们以前介绍的时候说过中间的临时对象是具有 常性的!我们可以验证一下:
验证的方式其实很简单,就是用A类型的引用去引用2,这里是不可以引用的,自定义类型不可能和内置类型隐式类型转换的,A类型引用的是中间的那个A类型的对象,所以得加const
上面是单参数的,也可以是全缺省参数的:
这里全缺省的支持一个参数的可以,那我们全部支持该如何写呢?
第一种情况:
这里的结果,其实是我们在C语言介绍过的逗号表达式!所以最终传过去的实际上是23,所以第一个参数接收到的是23,后面两个是缺省!这明显和我们的预期不一样,我们该如何解决呢?C++11提出了用{}解决!把要初始化的成员依次放在{}中并按照逗号隔开!
这样写代码是感觉方便了,但可读性变差了!如果不想发生这种隐式类型的转换我们可以在构造函数的前面加上explicit关键字,来禁止这种隐式类型的发生!
二、static成员
什么是static成员?
声明为static的类成员称之为类的静态成员。用static修饰的成员变量,被称为静态成员变量;用static修饰的成员函数,被称为静态成员函数。
我们以前在C语言的时候介绍过static变量和函数!
static修饰的局部变量,只有一份,存在静态区!static的局部变量的声明周期是整个程序!
static修饰的全局变量,将全局变量的外部链接属性修改为内部链接属性了!
static修饰的函数,和全局变量一样,将外部链接属性修改为内部链接属性了!生命周期还是整个程序!
如果有疑问请点击这里:点击我了解static
那类成员的会是咋样的呢?我们来看看:
静态成员变量一定要在类外面进行初始化!也就是在类里面声明在类外面进行定义!
static成员属于类,所有对象共享!(这里也就是只有一份)
cpp
class A
{
public:
static bool IsLetter(char& c)//静态成员函数
{
//...
}
private:
int a;
static int s;//静态成员变量 ----》这里是声明
};
int A::s = 10;//这是定义 ------>在类里面声明在类外面定义
以前的非static的成员变量是可以给一个缺省值的,这里我们能不能在声明的时候给一个缺省值呢?验证一下:
显然这里是不可以的!而我们前面刚刚介绍过这个缺省值是给初始化列表的!这里不可以给就说明静态的成员变量是不走初始化列表的!!!它的定义在类外面!
OK,了解到这里后,就可以解决一个面试题了!
实现一个类,计算程序中创建了多少个类对象?
由上面的介绍实现其类应该不难的!我们只需要整一个私有的static的成员变量,然后在调用构造、拷贝构造时记录即可!
cpp
class A
{
private:
static int _count;
public:
A()
{
++_count;
}
A(const A& a)
{
++_count;
}
~A()
{
--_count;
}
int GetCount()
{
return _count;
}
};
int A::_count = 0;
但这样有个问题就是,你想知道这个类到底创建了多少个对象时,就得自己用对象调用!也即是机子得创建一个对象才能调到GetCount()如下:
这样写其实有点麻烦,我们可以用一个匿名对象来解决!
总觉的这样写有些挫,那该如何处理呢?我们可以把GetCount()搞成static的,我们前面说过静态成员属于类,所有对象共享。也就是我们可以用类名 + ::访问 或 对象.访问了!
static的特性
1、静态成员属于所有类的对象共享,不属于某个具体的对象,存放在静态区!
2、静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明!
3、静态成员没有隐藏的this指针,不能访问任何非静态的成员
4、类静态成员可用 类名::静态成员 或者 对象.静态成员来访问
5、静态成员也是类的成员,受public、protected、private的访问限定符的限制
前两点已经在上面演示过了,下面我来把后面的几点解释一下:
静态成员没有隐藏的this指针,不能访问任何非静态的成员
this指针我们在一开始C++入门的时候就介绍过,他是某个对象的地址。由于static不属于具体对象所以没有this也就无法访问非静态成员了!
类静态成员可用 类名::静态成员 或者 对象.静态成员来访问
static成员属于类,即指定是哪个类的成员就可以访问!而又是所有对象共享即对象.也可以访问
静态成员也是类的成员,受public、protected、private的访问限定符的限制
即使你被static修饰了也还是成员收访问限定符的限制!
这里还有经常被问到的三个我问题,我们现在可以解决一下了!
1、静态成员函数可以调用非静态成员函数吗?
不可以!静态成员函数没有this指针,无法调到非静态成员函数!
2、非静态的成员函数可以调用静态的成员函数吗?
可以!静态成员函数属于类,只要指定类域即可调到!
3、全局变量和static全局变量的区别!
全局变量和static全局变量的生命周期一样都是整个程序的生命周期,不同的是static全局变量只能在当前的文件中使用,而一般的全局变量可以在其他文件可以使用!也就是作用域的范围不同!static被缩小到了当前的域里面而一般的全局没有!
三、友元
友元提供了一种突破封装的方式,又是提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用!友元分为友元函数和友元类!
友元函数
友元函数可以直接访问类的私有的成员,他是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需加上friend的关键字!
例如我们以前的日期类,打印日期只能调用Print(),而这样多少有些麻烦,C++流行用cout打印,但我们当时写的时候发现在类里面重载<<和>>的话,第一个参数是被默认的this给占了,导致得反着写:如下:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _out)
{
_out << _year << "年" << _month << "月" << _day << "日" << endl;
return _out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d << cout << endl;
return 0;
}
这样写真的是太挫了!而且还可能给人误导!因此我们当时在这里就引入了友元!即把流插入和流提取给放到类外面!利用友元来访问类里面的数据!如下:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
friend ostream& operator<<(ostream& _out, Date d);//类里面声明为友元函数,该函数就可以访问类的私有属性了
friend istream& operator>>(istream& _in, Date& d);
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
ostream& operator<<(ostream& _out, Date d)//在类外面实现为普通函数
{
_out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return _out;
}
istream& operator>>(istream& _in, Date& d)
{
_in >> d._year >> d._month >> d._day;
return _in;
}
这就是我们以前友元函数的用法!友元函数有一些注意的地方!
1、友元函数可以访问类的私有和保护成员,但不是类的成员函数!
2、友元函数不能用const修饰
3、友元函数可以在类定义时的任何地方声明,不受类访问限定符的限制
4、一个函数可以是多个类的友元函数
5、友元函数的调用与普通函数的调用原理相同
这里其他的都好理解,这里我就来解释一下第二点:
因为友元函数是要突破类的访问限定符去访问(可以是查询也可能是修改)类的成员属性的即破坏了封装,因此不可以用const修饰!
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非共有成员。
注意:
1、友元函数是单向的,不具有交换性。
例如下面的A和B两个类,在A中声明B时它的友元,那么在B中就可以直接访问A的私有成员了,反之在A中就不可以访问B的私有成员!
2、友元关系不能传递
如果C是B的友元,B是A的友元,则不能说明C是A的友元!
3、友元关系不能继承(后期进阶部分的继承那块再详细介绍)
简单的说一下,A是你爹的朋友不是你的朋友。后面在详细介绍!
cpp
class A
{
friend class B;//声明B是A的友元
public:
void test()
{
cout << _a << _b << endl;
}
private:
int _a;
int _b;
};
class B
{
public:
void tmp()
{
a._a = 10;
a._b = 20;
cout << a._a << " " << a._b << " " << _d << endl;
}
private:
double _d = 1.1;
A a;//声明A类为B类的一个成员
};
看结果
四、内部类
如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类是一个独立的类,他不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有优越的访问权限!
注意:
**内部类天生是外部类的友元!**既内部类可以直接访问外部类的成员,外部类则不可以!
内部类的特性
1、内部类可以定义在外部类的public 、protected 、private都是可以的!
2、注意内部类可以直接访问外部类中的static成员,无需外部类的对象或类名!
3、sizeof(外部类)= 外部类,和类不类没有关系!
cpp
class B
{
public:
class A//内部类天生就是外部类的友元类
{
public:
void test(const B& b)//内部类可以直接访问外部类的私有成员
{
cout << _a << " " << _b << " " << b._d << endl;
}
private:
int _a = 1;
int _b = 10;
};
void tmp()
{
cout << _d << endl;
}
private:
double _d = 1.1;
};
看结果:
五、匿名对象
假设我们现在有如下代码要调用:
cpp
class Solution
{
public:
int Sum(int x, int y)
{
return x + y;
}
};
我们是不是得用对象.呀!所以得创建一个对象,然后再去点!有点麻烦!我们有没简单一点的办法呢?这就是匿名对象!
匿名对象:类名();就是该类的匿名对象!
匿名对象的声明周期只有当前你们对象存在的那一行!
所以上面方法可以这样调用:
这是匿名对象的一种场景,其他的后面再说!
六、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,达到减少对象的拷贝,从而提高程序的效率,在一些场景下还是很有用的!
在同一个表达式中,连续的构造+构造/构造+拷贝构造/拷贝构造+拷贝构造会被合二为一!即如下:
1、构造 + 构造 ---> 会被优化为一次构造
2、构造 + 拷贝构造 ---> 会被优化为构造
3、拷贝构造 + 拷贝构造 ----> 会被优化为拷贝构造
OK,我们来看看(声明:小编的当前环境是vs2019Debug环境, 有的编译器优化可能不同)
cpp
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A()
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
if (this != &a)
{
_a = a._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a = 1;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
下面这个栗子,按正常来说会先用1构造一个临时对象,在用这个临时对象去拷贝构造a
cpp
A a = 1;//构造+拷贝构造会优化成一个构造
但编译器会优化成一个构造:
下面这个是正常的情况先是构造再是拷贝构造:
cpp
A a = 1;//先构造
f1(a);//拷贝构造
下面是优化情况:
cpp
f2();//构造+拷贝构造
我们接收一下就应该是构造+拷贝构造+拷贝构造!但编译器会把连续的拷贝构造优化成一个烤鹅比构造!
因此我们上面的总结是正确的!
OK,好兄弟我们下期再见!