🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++ 🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹
再谈构造函数
构造函数体赋值
在实例化对象时,编译器会通过调用构造函数给对象中的各个成员变量一个合适的初始值:
class Date {
public:
//构造函数
Date(int year = 2023,int month = 6,int day = 5)
{
//_year可以多次赋值
_year = year;
_year = 2024;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初始值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date {
public:
//构造函数
Date(int year = 2023,int month = 6,int day = 5)
//初始化列表
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
对于对象来说,初始化列表是对象成员变量定义初始化的地方。
在定义类时,类中的成员变量只是声明并不是定义初始化。
当我们在成员变量中加上const
,程序就不能正常运行了,这是因为const
变量必须在定义的时候初始化,const
只有一次初始化的机会,所以必须给成员变量找一个定义的位置,不然像const
类型的成员不好处理。所以在C++11中规定构造函数初始化列表是成员变量定义和初始化的地方。
class Date {
public:
//构造函数
Date(int year = 2023, int month = 6, int day = 5)
//初始化列表
:_year(year)
, _month(month)
, _day(day)
,_t(20) //true,初始化列表是成员变量定义的地方
{
_t = 30;//error,不能在构造函数体内初始化
}
private:
//成员变量的声明
int _year;
int _month;
int _day;
const int _t = 20; //这里只是缺省值,并不是初始化
};
int main()
{
Date d;//这里的定义是对象整体的定义
//成员变量在构造函数的初始化列表中定义并且初始化
}
注意:
1.哪个对象调用构造函数,初始化列表就是该对象所有成员变量定义的位置,且每个成员变量在初始化列表中只能出现一次。
因为任何类型的变量初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。
2.不管是否显式在初始化列表定义初始化成员变量,编译器对每个成员变量都会在初始化列表中进行定义初始化;当在初始化列表显式定义初始化成员变量的时候,使用初始化列表中的值。
例如当Date类的构造函数初始化列表为空时,也会先走初始化列表再走函数体。
class Date {
public:
//构造函数
Date(int year = 2023, int month = 6, int day = 5)
//初始化列表为空时,也会先走这里在进入函数体
{
}
/*Date(int year = 2023, int month = 6, int day = 5)
//初始化列表
:_year(year)
, _month(month)
, _day(day)
{
}*/
private:
//成员变量的声明
int _year;
int _month;
int _day;
};
3.有三个变量必须在初始化列表初始化:const变量
,int& 变量名
(引用也必须在定义的地方初始化),没有默认构造的自定义类型成员。
默认构造函数:无参的构造函数、全缺省的构造函数以及编译器自动生成的构造函数。
下面的程序中B类的构造函数不是默认构造函数,A类中的自定义类型成员变量_bb
会自动调用B类的构造函数,所以在A类中的初始化列表中要定义_bb
。const
成员变量_c
和引用成员变量_ref
都要在初始化列表中定义。
class B {
public:
//B的构造函数,不是默认构造函数
B(int a)
:_a(a)
{
cout << "B(int a = 20)" << endl;
}
//B的打印函数
void Print()
{
cout << _a << endl;
}
//B的析构函数
~B()
{
cout << "~B()" << endl;
}
private:
int _a = 0;
};
class A
{
public:
A()
:_a(2)
,_b(2)
,_c(3)
//,_ref(_c)//权限的放大,不可以,_c是const类型的变量
,_ref(_a)
,_bb(30)
{}
void Print()
{
cout << _a << "/" << _b << "/" << _c << "/" << _ref << "/" << endl;
_bb.Print();
}
private:
int _a;//声明
int _b;
const int _c;
int& _ref;
B _bb;
};
int main()
{
A a;
a.Print();
return 0;
}
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
观察下面的程序,最终的打印结果为多少呢?
class A {
public:
A(int a = 20,int b = 30)
:_a2(a)
,_a1(_a2)
{
}
//打印
void Print()
{
cout << _a1 << ":" << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A a;
a.Print();
return 0;
}
打印结果:
为什么会这样呢?因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,也就是说当我们在初始化列表中_a1(_a2)
时,_a2
的值并没有定义,并不是说_a2(a)
在前面_a2
就先定义了。我们可以将初始化列表中的顺序修改为_a1(a),_a2(_a1)
。所以建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。
5.最好在初始化列表里进行定义,在构造函数体内已经是赋值行为,即能用初始化列表尽量用初始化列表。
因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否显示定义初始化列表,都会先走初始化列表再进入函数体。
- 对于内置类型使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,如下面的代码:
int a = 10;//初始化列表
///
int a;//不使用初始化列表
a = 10;
- 对于自定义类型,使用初始化列表可以提高效率
class B {
public:
B(int a = 0)
:_a(a)
{}
private:
int _a = 0;
};
class A {
public:
//使用初始化列表
A()
:_b(20)
{
}
//不使用初始化列表
/*A()
{
B b(20);
_b = b;
}*/
private:
B _b;
};
int main()
{
A a;
return 0;
}
使用初始化列表时只需要在初始化列表那里调用一次B类的构造函数,而不使用初始化列表不仅要在初始化列表那里调用一次B类的构造函数,还要再函数体内调用一次构造函数和赋值重载运算符函数,所以使用初始化列表可以提高效率。
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量一个缺省值。
class A {
private:
//非静态成员变量可以给缺省值
int _a = 10;//缺省值
int _b = 20;//缺省值
};
初始化列表是对象成员变量初始化的地方,若是显示定义了初始化列表,则成员变量按照给定值进行初始化;如果没有显示定义,则用缺省值进行初始化;如果没有给定缺省值,则内置类型成员变量就是随机值。
explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换(C++98),C++98不支持多个参数的构造函数隐式类型转换,C++11支持。
class A {
public:
A(int a)
:_a(a)
{
}
private:
int _a;
int _b;
};
int main()
{
A a1(10);
A a2 = 10;
return 0;
}
我们看上面的代码:A a1(10);
调用构造函数实例化对象;A a2 = 10
实例化对象是一个隐式类型转换。如int i = 0; double d = i
这两句代码也会发生隐式类型转换,i
不是转换给d
,而是在类型转换中间会产生一个临时变量,临时变量类型是double
类型并且具有常性。所以A a2 = 10
也是先在中间产生一个具有常性的临时变量,然后将临时变量拷贝构造给a2
。
拷贝构造也有初始化列表,因为拷贝构造也是构造函数。
A a2 = 10
相当于A a(10); A a2(a)
,先构造再拷贝构造,但是现在经过编译器的优化,直接使用一个构造函数就完成了,相当于A a2(10)
。
那我们怎么知道中间产生一个临时变量呢?可以使用A& ref = 10
尝试,发现不能通过编译,但是如果改为const A& ref = 10
就可以了,因为临时变量具有常性,只能用const
类型接收。
在C++11中支持多参数的构造函数隐式类型转换,我们可以使用{}
括起来,A b = {1,2};
此时就支持多参数的隐式类型转换了。
如果我们不想让构造函数支持隐式类型转换,可以使用explicit
关键字,使用之后就不支持隐式类型转换了。用法如下:
class A {
public:
explicit A(int a)
:_a(a)
{
}
private:
int _a;
int _b;
};
int main()
{
A a1(10);
A a2 = 10;
return 0;
}
static成员
概念
声明为static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量;用static
修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
实现一个类,计算程序中创建了多少个类对象。
一个对象一定是一个构造函数或者拷贝构造函数实例化出来的。
我们可以定义一个全局变量count
来记录调用构造函数和拷贝构造函数的次数,可能和库中命名空间中的函数名或者函数符号相同,所以可以不展开命名空间以防冲突,程序如下:
#include <iostream>
int count = 0;
class A {
public:
A(int a = 10)
{
count++;
}
A(const A& a)
{
count++;
}
private:
int _a;
};
void func(A a)
{}
int main()
{
A a1;
A a2(a1);
func(a1);
std::cout << count << std::endl; //输出结果为3
return 0;
}
程序虽然可以,但是可能会出错。
我们可以增加一个静态成员变量static int count
,此时count
在静态区,不属于某个对象,属于所有对象,属于整个类。静态成员不在初始化列表初始化,在类外初始化。
使用静态成员变量计算程序中创建了多少个类对象。
#include <iostream>
class A {
public:
A(int a = 10)
{
_count++;
}
A(const A& a)
{
_count++;
}
static int GetCount()
{
return _count;
}
private:
int _a;
static int _count;
};
int A::_count = 0;
void func(A a)
{}
int main()
{
A a1;
A a2(a1);
func(a1);
std::cout << a1.GetCount() << std::endl;
return 0;
}
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
我们可以通过计算含有静态成员变量的类的大小来验证:
#include <iostream>
using namespace std;
class A {
private:
static int _n;
};
int main()
{
cout << sizeof(A) << endl; //输出1
}
A类的大小为1,静态成员变量_n
是存在静态区的,属于整个类,也属于类的所有对象,所以计算类的大小或是类对象的大小时,静态成员并不计入总大小之和。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
class A {
private:
static int _n;
};
int A::_n = 0;
虽然这里的静态成员变量是私有的,但是却突破了类域可以在类外直接对其进行访问,这是一个特例,不受访问限定符的限制,否则就无法初始化静态成员变量了。
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
匿名对象:我们在实例化对象的时候不能使用
A s();
,因为这样分不清是函数的声明还是调用构造函数,但是我们可以使用A()
,这个实例化出来的对象叫做匿名对象。特点:生命周期只在实例化对象这一行,没有名字。所以有时候调用函数的时候可以使用匿名对象,即用即销毁。
当静态成员变量为公有时,有以下几种访问方式:
#include <iostream>
using namespace std;
class A {
public:
static int _n;
};
int A::_n = 0;
int main()
{
A a;
cout << a._n << endl; //通过已实例化对象调用
cout << A()._n << endl; //通过匿名对象调用
cout << A::_n << endl; // 通过类名::静态成员变量调用
}
当静态成员变量为私有时,有几下几种访问方式:
#include <iostream>
using namespace std;
class A {
public:
static int GetN()
{
return _n;
}
private:
static int _n;
};
int A::_n = 0;
int main()
{
A a;
cout << a.GetN() << endl; //通过已实例化对象调用
cout << A().GetN() << endl; //通过匿名对象调用
cout << A::GetN() << endl; // 通过类名::静态成员变量调用,这里必须用静态成员函数
}
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
class A {
public:
static void Print()
{
cout << _a << endl;//error,静态成员函数没有this指针,无法访问非静态成员变量
cout << _n << endl; //true
}
private:
int _a;
static int _n;
};
int A::_n = 0;
含有静态成员变量的类一般都含有一个静态成员函数用来访问静态成员变量。
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
当静态成员变量设置为private
时,不能直接对其进行访问,要借助静态成员函数进行访问。
静态成员函数可以调用非静态成员函数吗?
不可以,非静态成员函数的第一个形参默认为
this
指针,而静态成员函数中没有this
指针,无法访问类对象的非静态成员变量,也无法访问类对象的非静态成员函数。非静态成员函数可以调用静态成员函数吗?
可以,因为非静态成员函数和静态成员函数都在类里,调用静态成员函数不需要
this
指针,直接调用就可以了。
我们可以看下面的题目:
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
我们可以通过构造函数来计算:
代码1:
class Solution{
public:
class Sum{
public:
Sum()
{
_sum += _n;
_n++;
}
};
int Sum_Solution(int n)
{
Sum a[n];
return _sum;
}
private:
static int _n;
static int _sum;
};
int Solution::_n = 1;
int Solution::_sum = 0;
代码2:
int Solution::Sum::_i = 1;
int Solution::Sum::_sum = 0;
class Solution {
public:
class Sum{
public:
Sum()
{
_sum += _i;
_i++;
}
static int GetNum()
{
return _sum;
}
private:
static int _i;
static int _sum;
};
int Sum_Solution(int n) {
Sum a[n];//调用n次构造函数,就计算出来了,变长数组,C99支持的,我们也可以用new
//Sum* ptr = new Sum[n];
return Sum::GetNum();
}
};
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元可以分为友元函数和友元类。
友元函数
我们在实现日期类的时候重载了流插入(<<)和流提取(>>),但是当重载成成员函数的时候this指针是第一个参数也就是左操作数,使用的时候不符合我们平时使用习惯,在实际使用中cout需要是第一个形参对象,所以我们可以将其重载为全局函数,通过在类中将其作为日期类的友元函数就可以访问类中的成员了。
#include <iostream>
using namespace std;
class Date {
//友元函数
friend istream& operator>>(istream& in, Date& d);//流提取
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;
};
//重载流提取
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//重载流插入
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
int main()
{
Date d1;
cin >> d1;
cout << d1;
}
cout是ostream中的一个全局对象,cin是istream中的一个全局对象。
友元函数说明:
-
友元函数可访问类的私有和保护成员,但不是类的成员函数
-
友元函数不能用const修饰
-
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
-
一个函数可以是多个类的友元函数
-
友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
#include <iostream>
using namespace std;
class Date {
friend class Time;//声明时间类是日期类的友元类,在Time类中可以访问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;
};
class Time {
public:
void PrintDate()
{
//直接使用Date类的对象访问Date类的私有成员
cout << d._year << d._month << d._day << endl;
}
private:
int _hour;
int _min;
int _sec;
Date d; //Date类的对象
};
int main()
{
Time t;
t.PrintDate();
}
- 友元关系是单向的,不具有交换性
上述代码中,Time
类是Date
类的友元类,Time
类可以访问Date
类中的私有成员,但是Date
类不能访问Time
类中的私有成员。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C是A的友元。
- 友元关系不能继承
内部类
概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
其实内部类就是外部类的友元类,即内部类可以通过外部类的对象来访问外部类中的所有成员,但是外部类不是内部类的友元类。
特性
-
内部类可以定义在外部类的public、protected、private都是可以的。
-
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
-
sizeof(外部类)=外部类,和内部类没有任何关系
#include <iostream>
using namespace std;
class A {
public:
//B天生就是A的友元类
class B {
public:
void Print(const A& a)
{
cout << _a << a._b << endl;//true
}
private:
static int _k;
int _b;
};
//这样写是错误的,A类不能访问B类中的成员
//void PrintB(const B& b)
//{
// cout << b._b;//error
//}
private:
static int _a;
int _b;
};
int A::_a = 1;//A中的静态成员变量初始化
int A::B::_k = 2;//B中的静态成员变量初始化
如果要访问内部类中的成员或者函数,要外部类::内部类::成员名
。并且要实例化内部类对象时,内部类必须在外部类的public中,private访问限定符不能实例化对象。空间独立但是某些操作受到访问限定符和类域的限制。
计算外部类的大小时,不包括内部类的大小。
编译器优化
我们通过下面的代码来认识一些编译器的优化:不同的版本有不同的优化方法,这里使用的版本是VS2022。
- 构造+拷贝构造可以优化为构造
C++98中支持单个参数的隐式类型转换,C++11中支持多个参数的隐式类型转换。
class A {
public:
A(int a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A a1 = 1;//构造+拷贝构造+优化 -> 构造
}
打印结果:
- 传值传参时没有优化,直接调用拷贝构造函数
class A {
public:
A(int a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{}
int main()
{
A a1 = 1;//构造+拷贝构造+优化 -> 构造
func1(a1);
}
打印结果:
func1(2)
和func1(A(2))
两个都是先构造一个临时对象,再把临时对象拷贝构造给func1
中的形参,编译器会进行优化。
class A {
public:
A(int a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{}
int main()
{
//A a1 = 1;//构造+拷贝构造+优化 -> 构造
func1(2);//构造+拷贝构造+优化 -> 构造
func1(A(2)); //构造 + 拷贝构造 + 优化->构造
}
打印结果:
func2(2)
、func2(A(2))
先生成一个临时变量,引用直接使用临时对象,无优化;func2(a1)
直接引用使用,无优化。
当函数的形参为引用类型时,如果不需要改变,最好加上const。
class A {
public:
A(int a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{}
//当函数的形参为引用类型时,如果不需要改变,最好加上const
//这里形参如果不加const会报错,因为传递的是临时变量,临时变量具有常性
void func2(const A& a)
{}
int main()
{
A a1 = 1;//构造+拷贝构造+优化 -> 构造
func2(2);//无优化
func2(A(2)); //无优化
func2(a1);//无优化
}
打印结果:
- 使用传值返回的时候也会发生优化
class A {
public:
A(int a = 20)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{}
void func2(const A& a)
{}
A func3()
{
A aa;
return aa;
}
int main()
{
A a1 = func3();//拷贝构造+拷贝构造+优化 -> 拷贝构造
}
两次拷贝构造优化为一次拷贝构造。
- 不会发生优化
class A {
public:
A(int a = 20)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
_a = a._a;
cout << "A& operator=(const A& a)" << endl;
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{}
void func2(const A& a)
{}
A func3()
{
A aa;
return aa;
}
int main()
{
A a1;
a1 = func3();//不能优化
}
总结:
1.接收函数返回值时尽量用拷贝构造方式接收,不要赋值接收
2.函数中返回对象时,尽量返回匿名对象
3.尽量使用const引用传参
再次理解类和对象
C++是面向对象的语言,面向对象三大特性:封装、继承、多态。
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
-
用户先要对现实中洗衣机实体进行抽象,即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有什么功能,即对洗衣机进行抽象认知的一个过程
-
经过上一步之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面向对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
-
经过上一步之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
-
用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,要体会到类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
封装
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起。通过访问限定符的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
比如我们乘火车,我们不需要关心票的分配机制等,只需要找到对应的座位即可,这就是封装的好处。
面向对象
可以看出,面向对象其实是在模拟抽象映射现实世界: