1.再谈构造函数
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.初始化列表
类的构造函数可以使用初始化列表来初始化成员变量。初始化列表位于构造函数的参数列表之后,使用冒号分隔。初始化列表的语法格式为:成员变量名1(初始值), 成员变量名2(初始值), … ,其中初始值可以是一个表达式或者是一个常量值。
class Date
{
public:
Date(int year,int month,int day)
//类对象的成员变量定义的位置
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//类对象的定义
Date d1(2023,5,20);
return 0;
}
初始化列表本质上就是类对象的成员变量定义的地方。为什么类对象的成员变量一定就需要定义的地方呢?且听下面分析。
1.2.1初始化列表的特点
1、类中如果包含以下类型成员变量,必须放在初始化列表位置进行初始化。
1、引用成员变量
2、const成员变量
3、自定义类型成员变量(且该类没有默认构造函数)
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
int x = 1;//这是声明缺省值
};
2、每个成员函数有且只有一次初始化。
3、尽可能的使用初始化列表去做初始化工作,因为不管我们是否使用初始化列表,对于自定义类型成员变量来说,一定会先使用初始化列表来进行初始化。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
4、 初始化列表并不能完全替代构造函数体内赋值。
class Stack
{
public:
Stack(int capacity = 10)
: _a((int*)malloc(capacity * sizeof(int)))
,_top(0)
,_capacity(capacity)
//初始化列表并不能完成所有初始化场景
{
//判断有效性
if (nullptr == _a)
{
perror("malloc申请空间失败");
exit(-1);
}
// 要求数组初始化一下
memset(_a, 0, sizeof(int) * capacity);
}
private:
int* _a;
int _top;
int _capacity;
};
5(重点)、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
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;
}
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
这里我们来分析一下代码,首先,会调用初始化列表来初始化对象的成员变量。由于类的成员变量的声明顺序为a2、a1。所以,这里初始化列表先用a1的值来初始化a2。但是,a1此时还是随机值,故a2的值为随机值。而a1会被初始化成1。答案为D。
1.3.explicit关键字
这里在正式介绍explicit关键字前,先介绍一下类类型的隐式类型转换。请看下面样例。
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2; // 隐式类型转换,整形转换成自定义类型
A& aa3 = 2;
// error C2440: “初始化”: 无法从“int”转换为“A &”
const A& aa3 = 2;
return 0;
}
pxplicit关键字可以禁止这类的类型转化。在A的构造函数前加上explicit关键字后,aa2和aa3便不能定义,编译器报错,没有显式的类型转化。
2.静态成员
2.1.静态成员的概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
class A
{
private:
static int _count;
};
//必须在类外定义
//只有定义时,可以突破一次类域限制
int A::_count = 0;
访问 _count变量必须是在类域内才能访问。当我们需要在类域外访问该静态成员变量时,可以借助静态成员函数来实现访问。
class A
{
public:
static int Getcount()
{
return _count;
}
private:
static int _count;
};
//必须在类外定义
//只有定义时,可以突破一次类域限制
int A::_count = 0;
int main()
{
//需要指明类域进行访问静态成员函数
cout << A::Getcount() << endl;
return 0;
}
2.2.经典试题:计算程序中运行到cout处时,创建出了多少个类对象。
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;
A a1;
void Func()
{
A a;
static A aa;
}
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
Func();
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}
在程序一开始,便在全局定义了第一个类对象a1,所以第一个cout执行结果为1。紧接着,定义了两个局部类对象分别是a1,a2。需要注意的是a1和全局的a1不冲突,因为处于不同的作用域范围。执行Func函数,定义了一个局部类对象a和静态类对象aa。Func函数调用结束,局部对象销毁。而静态对象的是定义在静态区的,不会随着函数调用的结束而销毁。所以,第二个cout执行结果为4。
2.3.静态成员的特性
1、静态成员是被所有类的对象共享的,它不属于某个具体的类对象,它是属于整个类,存放在静态区中。
2、静态成员变量必须定义在类外面,定义时不添加static关键字,类中存放的是声明。
3、类静态成员可以用类名::静态成员或者对象.静态成员来进行访问。
4、因为静态成员函数是没有隐含的this指针,所以不能访问非静态的类成员。
5、静态成员也是类成员,会受到public、private、protect访问限定符的限制。
2.4.两个关于静态成员函数和非静态成员函数调用问题
1、静态成员函数可以调用非静态成员函数吗?
答案是不可以,因为静态成员函数没有this指针。调用非静态成员函数的参数部分需要隐含this指针,如果在非静态成员函数内部有对类的成员变量进行访问就会报错。
2. 非静态成员函数可以调用类的静态成员函数吗?
答案是可以。因为非静态成员函数也是类一部分,在类的作用域内调用静态成员函数是OK的。
2.5.简单提及单例类的思想
假设我们需要设计一个只能在栈上或者堆上开辟类对象的方法。我们需要怎么做呢?首先,我们要讲构造函数私有,通过静态成员函数的调用来实现这一功能。
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
//无法直接构造
A()
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
A aa1 = A::GetStackObj();//栈上对象
A aa2 = *(A::GetHeapObj());//堆上对象
return 0;
}
3.友元
3.1.友元的概念
友元(friend)是C++中的一个关键字,用于实现类之间的访问控制。在C++中,类可以将其他类或函数声明为友元,从而让它们访问自己的私有成员和保护成员。被声明为友元的类或函数可以直接访问另一个类的私有成员和保护成员,而不需要通过该类的公有接口来访问。友元的使用可以增加程序的灵活性和可扩展性,但也会增加程序的耦合度和不安全性。因此,在使用友元时需要谨慎考虑,并遵循最小暴露原则,尽可能地减少对其他类的访问控制。
3.2.友元函数
在前文日期类的实现中,就已经提到了<<运算符和>>运算符重载需要声明友元。这是因为,重载类对象成员函数,this指针会占用默认的第一个参数。没法实现类似cout << obj这样使用库函数。这时候就需要友元函数声明,并将运算符重载到全局。
//声明
class Date
{
friend ostream& operator <<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
}
//定义
ostream& operator <<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
友元函数的说明:
1、友元函数不能被const修饰。
2、友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
3、一个函数可以使多个类的友元函数。
4、友元函数的调用和普通函数的调用原理相同
3.3.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的所有成员函数。友元关系是单向的,不具有交换性。友元也不具备传递性,即若B是A的友元,C是B的友元,不能说明C是A的友元。友元关系不能继承,这里我们暂时不谈,需要了解有这一概念即可。
class A
{
friend class B;//声明B是我的友元,B内可以访问A的成员
//而A内不能访问B的成员
private:
int _a;
double _d;
protected:
char _c;
};
class B
{
public:
int GetAi()
{
return _ca._a;
}
double GetAd()
{
return _ca._d;
}
char GetAc()
{
return _ca._c;
}
private:
int _i;
A _ca;
};
4.内部类
内部类顾名思义就是一个类定义在另一个类的内部。内部类是一个独立的类,它并不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。内部类是外部类的友元,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
4.1.内部类的特性
1、内部类可以定义在外部类的任意处,无论public、private、protected都是可以的。
class A
{
private:
static int k;
int h;
public:
class B// B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//可以直接访问static成员
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B _b;
cout<<sizeof(A)<<endl;//4字节
return 0;
}
这里k是存储在静态区中,所以不计入sizeof的大小。内部类可以直接访问外部类的static成员,不需要外部类的对象/类名。
5.匿名对象
C++中的匿名对象指的是没有命名的临时对象,该对象是在表达式中创建的,用于执行某些操作并返回结果
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A a(10);
A(10);//匿名对象,声明周期仅在表达式行内
return 0;
}
5.1.匿名对象生命周期的延长
int main()
{
A(10);//匿名对象,声明周期仅在表达式行内
const A& ra = A(10);
A(10)
return 0;
}
当我们将一个匿名对象赋值给一个 const 引用时,编译器会将这个匿名对象的生命周期延长到引用的作用域范围内。这是因为当我们定义一个 const 引用时,编译器会在内存中分配一个临时变量来存储这个引用所指向的值,而这个临时变量的生命周期和 const 引用的作用域范围相同。因此,当我们将一个匿名对象赋值给 const 引用时,编译器会将这个匿名对象的生命周期延长到 const 引用的作用域范围内,以保证 const 引用能够正确地引用这个对象。这样做可以避免出现悬垂指针的问题,保证程序的安全性和正确性。
6.关于构造函数的补充
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Func1(A aa)
{
}
void Func2(const A& aa)
{
}
int main()
{
A aa1;
Func1(aa1);
Func2(aa1);
return 0;
}
首先,创建aa1对象会调用一次构造函数。而调用Func1函数,在传参的过程中会产生临时变量。将形参拷贝给实参,会调用拷贝构造。而Func2传参并不会调用拷贝构造,因为传的是引用,不会开辟临时空间。
请看下面的场景。
//class A...
//这里我就不再写了
void Func1(A aa)
{}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra1 = Func5(); // 拷贝构造+拷贝构造 ->优化为拷贝构造
cout << "==============" << endl;
A ra2;
ra2 = Func5();
return 0;
}
对于同一行内的连续的构造+拷贝构造编译器会进行优化,优化为直接构造。而ra2这样的定义的方式,对于内置类型来说会连续调用三次构造函数,对程序性能有所影响。建议对于自定义类型的定义采用ra1类似的方式。