片头
在上一篇中,我们学习了类和对象(中),今天,我们继续深入学习类和对象,准备好了吗?我们开始咯~
一、再谈构造函数
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 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
还记得我们之前写过的Stack类吗?
typedef int DataType;
class Stack {
public:
Stack(size_t capacity = 4) {
_array =(DataType*) malloc(sizeof(DataType) * (capacity));
if (_array == nullptr) {
perror("malloc fail!\n");
exit(1);
}
_capacity = capacity;
_top = 0;
}
void Push(DataType data) {
// CheckCapacity();
_array[_top] = data;
_top++;
}
void Pop() {
_top--;
}
~Stack() {
if (_array) {
free(_array);
_array = NULL;
}
_capacity = 0;
_top = 0;
}
private:
DataType* _array;
int _capacity;
int _top;
};
class MyQueue {
private:
Stack Pushst;
Stack Popst;
int _top;
};
int main() {
MyQueue q;
return 0;
}
假设,我们这里不提供栈的默认构造,提供一个带参的构造函数
运行一下:
这个时候,Stack不具备默认构造,MyQueue也无法生成默认构造,只能采用初始化列表来解决。
这样的话,Stack类里面的初始化是带参构造,我就可以用初始化列表来传递参数,调用Stack类的构造函数,进而完成Pushst和Popst的初始化。
初始化列表的本质:每个对象中成员定义的地方
那初始化列表和函数体内的初始化能不能混着用呢?
答案是:可以!
一般情况下,能写初始化列表就写初始化列表,不需要混用,哈哈~
注意:
(1)每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
(2)类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
比如:
①引用成员变量
对于引用类型的成员变量,我们必须在声明变量时就给出它的引用对象
②const成员变量
为什么呢?因为const必须在定义时初始化,const变量只有1次初始化的机会。所以正确写法应该是这样:
③没有默认构造函数的自定义类型成员变量
对于没有默认构造函数的自定义类型成员变量, 我们在初始化时需要传参。
(3)尽量多使用初始化列表去初始化,因为不管你是否使用,对于自定义类型成员变量都一定会先使用初始化列表去初始化
例如:
可以看到,Date类里有一个Time类的成员函数,虽然我们没有在初始化列表中初始化它,它也会使用初始化列表去调用自己的默认构造函数。
如果该自定义类型成员变量的构造函数不是无参的或者全缺省的,我们就需要手动将该变量添加至初始化列表中并给出参数。
总之,我们平时写构造函数的时候尽量用初始化列表来初始化成员变量即可。
你可能会问:那既然初始化列表这么重要,是不是函数体内的初始化就可以不写了呢?
不是的,康康下面这个例子~
所以,即便是初始化列表很重要,也有它实现不了的地方,比如:memset函数只能放在函数体内~
(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();
return 0;
}
猜猜看,结果是啥?
哈哈哈哈,没想到吧,结果为: 1 和 随机值
为什么呢?因为_a2比_a1先声明,所以在初始化列表中也是_a2先初始化,但是_a1此时是随机值,_a1拷贝构造_a2,所以_a2也是随机值。接着初始化_a1,用传递过来的形参a的值"1"去拷贝构造_a1。
最后,拷贝构造函数因为也是构造函数,所以它也有初始化列表
1.3 explicit关键字
我们知道,内置类型变量在发生类型转换的时候会生成一个临时的常性变量,例如:
int a = 1;
double b = a;
但是,类型转换不止能发生在内置类型中,内置类型也可以转换成自定义类型,这里就和构造函数有联系了。
一个类的构造函数,不仅起到初始化成员变量的作用,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数来说,它还具有类型转换的作用。
或许没看懂,那就举一个例子~
像这样,对象aa1在创建时正常调用构造函数,aa3又是为什么呢?为什么一个自定义类型能被内置类型初始化?
我们刚刚说过,内置类型在发生类型转换的时候会生成一个临时的常性变量,这里也一样
首先,编译器使用"3"作为参数,调用构造函数,创建一个临时对象,再用这个临时对象调用拷贝构造函数对aa3赋值。
所以aa1只调用了一次构造函数,而aa3这一行代码调用了一次构造函数和一次拷贝构造函数。
这种方式既影响代码可读性,又增加了消耗,有什么办法可以禁止构造函数类型转换呢?
这里引入explicit关键字, 在构造函数的前面加上它,即可禁止类型转换了。
这是单参数的构造函数,对于第一个参数无缺省值的半缺省构造函数也是同理
只要是只传递一个参数的构造函数,用这种方法都会发生类型转换
另外,对于需要传递多个参数的构造函数,在C++11后也开始支持类型转换了,例如:
或者这样:
如果不想类型转换,用explicit修饰构造函数即可。
二、static成员
2.1 概念
static修饰的类成员称为类的静态成员,static修饰的成员变量称为静态成员变量,static修饰的成员函数称为静态成员函数。静态成员变量一定要在类外进行初始化。
小试牛刀:猜猜看,A类的大小为多少?
class A {
public:
A() {
++_scount;
}
A(const A& t) {
++_scount;
}
~A() {
--_scount;
}
private:
//声明
int _a1 = 1;
int _a2 = 1;
//静态区,不存在对象中
static int _scount;
};
int main() {
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
结果为:
8
为什么呢?因为static修饰成员变量, 这个变量不存在对象中,静态成员变量存放在静态区。
总结:对象当中,只存普通的成员变量,不存成员函数(公共代码区),也不存静态成员变量(静态区)
Q1:那普通的成员变量可以给缺省值,静态成员变量可以给缺省值吗?
A:不行!静态成员变量不能给缺省值,因为缺省值其实是给初始化列表的,初始化列表是为了初始化对象的成员。静态成员变量在静态区不存在对象中,不走初始化列表。
Q2:那么,静态成员函数在哪里定义呢?
ps:如果静态成员变量/静态成员函数只声明,不定义,编译器会报错!
另外,静态成员属于所有整个类,属于所有对象
面试题:实现一个类,计算程序中创建出了多少个类对象
class A {
public:
A() {
++_scount;
}
A(const A& t) {
++_scount;
}
~A() {
--_scount;
}
public:
//声明
int _a1 = 1;
int _a2 = 1;
//它在静态区不在对象中,不走初始化列表
//静态成员属于所有整个类,属于所有对象
static int _scount;
};
//定义
int A::_scount = 0;
A func() {
A aa4;
return aa4;
}
int main() {
A aa1;
cout << sizeof(aa1) << endl;
A aa2;
A aa3(aa1);
func();
cout << A::_scount << endl;
return 0;
}
咦?好奇怪,我们只创建了4个对象,为啥打印出来对外结果为5呢?
因为函数传值返回还会生成一个拷贝的临时对象。这个临时对象创建时,会调用类的拷贝构造函数。
刚刚这些情况都是建立在静态成员变量为公有(public)的情况下,如果静态成员变量为私有(private),是不能直接访问的。
那怎么办呢?别着急,虽然静态成员变量不能直接使用了,但是我们可以直接使用静态成员函数呀!
写一个静态成员函数,用来获取静态成员变量_scount
总结:
(1)static修饰变量,会影响生命周期
(2)static修饰函数,静态成员函数没有this指针,只能访问静态成员
当静态成员变量私有时,我们在main函数里面怎么访问_scount呢?
2.2 特性
根据上面的图,我们可以得出以下几点:
(1)静态成员不属于某个对象,而是属于所有对象、属于整个类,存放在静态区
所以我们上面可以直接使用类名和作用域限定符来访问静态成员函数GetCount,当然也可以创建一个对象来访问静态成员函数,但是显得很多余了
(2)静态成员变量必须在类外进行定义和初始化,不需要添加static,在类中声明时才需要加
(3)静态成员函数没有隐式的this指针形参,所以不能访问任何非静态成员
(4)静态成员也是类的成员,受public、protected和private访问限定符的限制
像上面,如果_count是私有的,我们只能用函数来获取它,现在它是公有的,我们就可以直接访问
小练习1
emmm,这个看上去好像没什么思路~
用常规方法是行不通的,那就用我们今天学的知识吧~
本质:不论是循环还是递归,我们都要走n次
思路:那我们可以调用n次构造函数,怎么调用n次呢?可以写一个变长数组,元素类型为Sum类的对象。(我定义了一个数组,相当于定义了n个Sum对象;我定义1个对象,就调用1次构造,我定义n个对象,就调用n次构造)
接着我可以在函数里面计算出结果,每次构造都要取到相同的值,那可以定义2个静态成员变量,一个为1,一个为0,再提供一个公有的静态成员函数用来最后返回的结果。
步骤:定义一个变长数组,这个数组里面存放的是n个Sum类的对象。在外面定义Sum类,在类里面定义2个静态成员变量,分别表示加数(初始化为1)和累加和(初始化为0)。在Sum类里面的构造函数计算出结果,再提供一个公有的静态成员函数,用来返回最后的结果
①先定义一个变长数组(元素均为Sum类的对象)
//变长数组
Sum arr[n];
②再定义Sum类,在Sum类里面定义2个私有静态成员变量、1个构造函数和1个静态成员函数
class Sum{
public:
Sum() //Sum类的默认构造函数
{
_ret+=_i;//累加和
_i++;//每次加完后,_i自增一次
}
static int GetSum() //返回最终累加的结果
{
return _ret;
}
private:
//声明
static int _i;//表示加数
static int _ret;//表示累加和
};
//定义
int Sum::_i = 1;//表示加数,初始化为1
int Sum::_ret = 0;//表示累加和,初始化为0
③最后在Solution类里面返回Sum类的GetSum()函数的结果
return Sum::GetSum();//返回Sum类的GetSum()函数的结果
完整代码如下:
class Sum{
public:
Sum() //Sum类的默认构造函数
{
_ret+=_i;//累加和
_i++;//每次加完后,_i自增一次
}
static int GetSum() //返回最终累加的结果
{
return _ret;
}
private:
//声明
static int _i;//表示加数
static int _ret;//表示累加和
};
//定义
int Sum::_i = 1;//表示加数,初始化为1
int Sum::_ret = 0;//表示累加和,初始化为0
class Solution {
public:
int Sum_Solution(int n) {
//变长数组
Sum arr[n];
return Sum::GetSum();//返回Sum类的GetSum()函数的结果
}
};
方法二:可以把Sum类改成Solution类的友元并且私有化,别人就用不到Sum这个类了。
class Solution {
class Sum //Sum类是Solution类的友元
{
public:
Sum() //Sum类的构造函数
{
_ret += _i;//累加和
_i++;//每加完一次,_i自增一次
}
};
static int _i;//_i放在了Solution类里面
static int _ret;//_ret放在了Solution类里面
public:
int Sum_Solution(int n) {
Sum arr[n]; //变长数组
return _ret; //直接将累加和的结果返回
}
};
int Solution::_i = 1;//_i在Solution类外定义
int Solution::_ret = 0;//_ret在Solution类外定义
我们可以对比一下:
通过两幅图的对比,我们可以看到:
之前,Sum和Solution是两个独立的类域,把 _i 和 _ret 放到Sum类里面, 外面访问不了私有的 _i 和 _ret,就只能提供一个GetSum()函数。
现在把Sum类放到Solution类的内部,变成友元专属,把 _i 和 _ret 放在Solution类里面,好处:
①首先,Solution类可以直接使用成员变量,不需要调用GetSum()函数,直接就能访问
②内部类是外部类的友元,内部类也可以访问私有的成员变量。
③内部类是外部类的私有类,只能由这个外部类访问,其他的类不允许访问,提高了封装性和安全性。
三、友元
当我们在类外定义了一个函数,想要访问类中的私有成员变量怎么办呢?这里就涉及到友元。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元又分为:友元函数和友元类
3.1 友元函数
我们知道,cout和流插入运算符可以实现将内置类型打印的效果,那么假设我想将流插入运算符重载,让自定义类型也可以使用呢?
我们试试在类中实现流插入运算符的重载函数
可以看到,实现的重载函数没有起效
这是因为,成员函数的第一个变量是this指针,在重载函数中对应第一个变量的是第一个操作数
所以如果像这样写就可以正常运行了
但是这样很奇怪,和平时用cout一点也不一样,有没有别的办法呢?
我们只好在类外去实现流插入运算符的重载函数了,但是类外的函数又没办法访问类内的私有成员
像这种必须定义在类外,但是又需要访问类内的私有成员的函数,就需要友元来解决了。
友元函数可以直接访问类内的私有成员,需要在类的内部进行声明,声明时需要加friend关键字。
需要说明以下几点:
(1)虽然友元函数可以访问类的私有和保护成员,但不是类的成员函数
(2)友元函数不能用const修饰
(3)友元函数可以在类的任何地方声明,不受类访问限定符限制
(4)一个函数可以同时是多个类的友元函数
(5)友元函数的调用和普通函数一样
3.2 友元类
和友元函数类似,我们也可以在类中声明一个友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类的非公有成员
需要注意以下几点:
(1) 友元关系是单向的,不具有交换性
比如:Date类是Time类的友元,所以可以直接在Date类中访问Time类的私有成员变量。但是不代表Time类是Date类的友元,不能在Time类中访问Date类的私有成员变量
(2)友元关系不能传递
例如:A是B的友元,B是C的友元,不代表A就是C的友元了
(3)友元关系不能继承
这里讲到继承再给大家介绍
四、内部类
4.1 概念
如果一个类定义在另一个类的内部,这个定义在内部的类就称为内部类。
内部类是一个独立的类,它不属于外部类,我们更不能通过外部类的对象去访问内部类的成员。
外部类对内部类没有任何优越的访问权限
来,先看看这个例子~
动动脑:想一想sizeof(A)的值为多少?
class A {
public:
void func()
{}
private:
static int k;
int h;
//内部类
class B {
public:
void foo(const A& a) {
cout << k << endl;
cout << a.h << endl;
}
private:
int _b;
};
};
int main() {
cout << sizeof(A) << endl;
}
答案是:4
为什么呢?因为虽然B类在A类的里面,但是它不属于A类(外部类),B类是一个独立的类。就像是B类单独写在外面一样。相当于是2个平行的类。
那么,将B类放在A类,B类会受到什么影响呢?
总结-----内部类与外部类的关联在于:
(1)内部类受外部类的类域限制
例如我们想创建一个内部类类型的变量,需要用作用域限定符
(2)内部类天生就是外部类的友元,但是外部类不是内部类的友元
4.2 特性
(1)内部类定义在外部类的public、protected和private中都是可以的
(2)内部类可以直接访问外部类中的静态成员,而不需要借助外部类的对象或类名
(3)外部类的大小不包括内部类
例如:
可以看到,外部类A的大小并没有包括内部类B,所以可以知道,内部类的空间也是独立的
五、匿名对象
有时候我们可能只需要调用一次某个类的成员函数,为此如果特意去创建一个对象的话就太麻烦了。
我们就可以用到匿名对象。我们平时创建一个对象可能是这样的:
如果要创建一个匿名对象的话,是这样的:
顾名思义,匿名对象在创建的时候是不用取名字的。
匿名对象的特点在于:它的生命周期只在这一行,一旦程序走到下一行,就会自动调用析构函数销毁。
假设此时我们要调用一次A类中的Print函数,就可以用匿名对象去调用,而不是特意创建一个对象。
对于各种一次性对象的创建,我们都可以使用匿名对象。
小练习2
emmm,对于这种题,很多小伙伴的第一反应就是运用赋值运算符重载来解决,用当前的日期减去1月1日,但是很费时间,而且代码很多,所以这种方法不推荐~
思路:计算1~N月的累加和,如果为闰年,那么需要单独判断
代码如下:
#include <iostream>
using namespace std;
//计算日期到天数转换
int main() {
int year, month, day;
cin >> year >> month >> day;
//统计1~12个月的累加和
int Month_day[13] = { 0,31,59,90,120,151,181,212,243,273,304,334,365 };
//天数 = 1~month-1的累加和 + 日期
int n = Month_day[month - 1] + day;
//如果输入的月份>2并且该年为闰年,那么天数+1
if (month > 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
n += 1;
}
//输出最终结果
cout << n << endl;
return 0;
}
补充:
class A {
public:
A() {
cout << "A()" << endl;
}
A(const A& aa) {
cout << "A(const A& aa)" << endl;
}
~A() {
cout << "~A()" << endl;
}
};
void f2() {
//局部的静态第一次调用时构造初始化
static A aa3;
}
//全局对象, 在main函数之前构造
A aa0;
int main() {
//后定义先析构
A aa1;
A aa2 = aa1;
f2();
cout << "-------------------------" << endl;
f2();
cout << "-------------------------" << endl;
return 0;
}
运行结果如下:
总结:
① 对象的析构顺序和栈的后进先出(LIFO)类似,后定义的先析构
② 全局对象,在main函数之前构造初始化
③ 局部的静态对象第1次调用时构造初始化,后续如果再调用,不会再初始化。它的生命周期是整个工程。
片尾
今天我们学习了类和对象(下),希望看完这篇文章能对友友们有所帮助!!!
求点赞收藏加关注!!!
谢谢大家!!!