再谈构造函数
回顾函数体内赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date{
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
注意:虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
初始化列表
格式
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
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. 类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量 、const成员变量 、自定义类型成员(且该类没有默认构造函数时)
为什么呢?
首先,当类的对象实例化时,会调用构造函数,如果有对成员变量赋值的操作,就会对成员变量进行赋值。引用成员变量、const成员变量必须在定义的时候初始化,不能再进行赋值操作,因为引用必须初始化,且当一个引用一旦引用了一个实体,就不能通过赋值操作再引用实体;而const 成员变量的值一旦被设定就不能通过赋值操作改变。
class B{
public:
//正确写法:
//初始化列表:对象成员变量定义的位置,符合初始化只能初始化一次的要求。
B(int a, int ref):_ref(ref),_n(10)
{}
//错误写法:
B(int a, int ref) {
//这里并不是对象成员变量定义的位置,是函数体内赋值,对成员变量赋值的地方。
_ref=ref;
_n=a;
}
private:
//这里只是成员变量声明的地方,不是定义的地方。
int& _ref; // 引用
const int _n; // const
};
int main(){
// 对象实例化
B bb1(10, 1);
B bb2(11, 2);
return 0;
}
对于自定义类型成员变量_aobj,如果它有默认构造函数,那么可以不显式地在初始化列表中初始化它,编译器会自动调用默认构造函数。如果没有默认构造函数,则必须在初始化列表中显式调用其构造函数。
A 有默认构造函数:
class A {
public:
A(int a = 0) : _a(a) {}
private:
int _a;
};
class B {
public:
// A 有默认构造函数,所以可以不显式初始化 _aobj
B(int a, int ref) : _ref(ref), _n(10) {}
private:
A _aobj; // 有默认构造函数
int& _ref; // 引用
const int _n; // const
};
A 没有默认构造函数:
class A {
public:
A(int a) : _a(a) {}
private:
int _a;
};
class B {
public:
// 在初始化列表中显式调用 A 的构造函数
B(int a, int ref) : _aobj(a), _ref(ref), _n(10) {}
private:
A _aobj; // 无默认构造函数
int& _ref; // 引用
const int _n; // const
};
对于内置类型(如 int
),如果初始化列表中没有显式初始化,则使用缺省值(如果有的话)或者未定义值(如果没有缺省值)。如果初始化列表中显式初始化了内置类型成员,则使用指定的值。
class B {
public:
// 写法1:_x 未在初始化列表中显式初始化,使用缺省值 1
B(int a, int ref) : _ref(ref), _n(10) {}
// 写法2:_x 在初始化列表中显式初始化为 2
B(int a, int ref, int x) : _ref(ref), _n(10), _x(2) {}
private:
int& _ref; // 引用
const int _n; // const
int _x = 1; // 缺省值为 1,但仅在未显式初始化时使用
};
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
我们先来看一个代码:
typedef int DataType;
class Stack {
public:
Stack(size_t capacity = 10) {
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array) {
perror("malloc 申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
// ... 其他成员函数
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
class MyQueue {
public:
MyQueue() {}
MyQueue(int capacity) {}
private:
Stack _pushst;
Stack _popst;
};
int main()
{
MyQueue q1;//这里的q1._pushst和q1._popst成员变量的capacity都是10。
return 0;
}
即使在MyQueue类中的构造函数里什么操作都没有,我们也没有写初始化列表,当MyQueue类实例化对象时调用到MyQueue构造函数了,也会调用到_pushst和_popst成员变量的构造函数,因为MyQueue类中所有的成员变量还是会走初始化列表。但是如果我们显示化地写处初始化成员列表,它就会走我们写的显示化的初始化成员列表,这样我们就可以根据自己的需求设置_pushst和_popst成员变量的容量大小了(_capacity):
class MyQueue {
public:
// 默认构造函数,不显式初始化_pushst和_popst,它们将使用Stack的默认构造函数(capacity为10)
MyQueue() {}
// 有参构造函数,使用初始化列表显式初始化_pushst和_popst的capacity
MyQueue(int capacity) : _pushst(capacity), _popst(capacity) {}
private:
Stack _pushst;
Stack _popst;
};
int main() {
MyQueue q1; // 使用默认构造函数,q1._pushst和q1._popst的capacity都是10
MyQueue q2(100); // 使用有参构造函数,q2._pushst和q2._popst的capacity都是100
return 0;
}
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;
}
最后它会输出什么呢?答案:1 随机值
因为它先声明的是_a2,所以初始化列表中也会先声明_a2。但此时_a1还没有被赋值(所以是随机值),所以_a2会被赋值为随机值。然后再初始化_a1,把a赋给_a1,所以_a1就是1了,所以最后会输出1 随机值。
所以建议声明的顺序和定义的顺序保持一致。
explicit关键字
我们来看这样的代码:
#include <iostream>
using namespace std;
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(int a)
A aa2 = 2; // 隐式类型转换:2 转换为 A 的临时对象,然后拷贝构造 aa2
// 注意:以下代码会导致编译错误,因为不能从 int 直接转换为 A&
// A& aa3 = 2; // error C2440: “初始化”: 无法从“int”转换为“A &”
// 但是,可以转换为 const A&,因为临时对象具有常性
const A& aa3 = 2; // 隐式类型转换:2 转换为 A 的临时对象,然后绑定到 const A& aa3
return 0;
}
在这个例子中,A aa2 = 2;
发生了隐式类型转换,2
被用来构造一个 A
类型的临时对象,然后这个临时对象通过拷贝构造函数来初始化 aa2
。然而,在同一个表达式里出现连续构造时,现代编译器通常基本都会有这样的优化。
如果我们不想要这样的隐式转换发生该怎么办呢?
在初始化列表前面加一个expilicit就可以了:
#include <iostream>
using namespace std;
class A {
public:
explicit 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(int a)
// 下面的代码会报错,因为 explicit 禁止了隐式类型转换
// A aa2 = 2; // error C2440: “初始化”: 无法从“int”转换为“const A &”
// 同样,即使绑定到 const A&,隐式类型转换也被禁止
// const A& aa3 = 2; // error C2440: “初始化”: 无法从“int”转换为“const A &”
// 正确的做法是显式调用构造函数
A aa2(2); // 直接调用构造函数 A(int a)
const A& aa3 = A(2); // 显式创建临时对象并绑定到 const A&
return 0;
}
static成员
概念
声明为 static
的类成员称为类的静态成员。用 static
修饰的成员变量称为静态成员变量;用 static
修饰的成员函数称为静态成员函数。
特性
1.静态成员也是类的成员,受public、protected、private 访问限定符的限制
2.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
class A{
public:
A(){
++_scount;
}
A(const A& t) {
++_scount;
}
~A(){
--_scount;
}
private:
// 成员变量 -- 属于每个一个类对象,存储对象里面
int _a1 = 1;
int _a2 = 2;
// 静态成员变量 -- 属于类,属于类的每个对象共享,存储在静态区,生命周期是全局的
static int _scount;
};
3. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。静态成员变量不能通过缺省值初始化,因为静态成员没有初始化列表
class A{
public:
A(){
++_scount;
}
A(const A& t) {
++_scount;
}
~A(){
--_scount;
}
private:
// 成员变量 -- 属于每个一个类对象,存储对象里面
int _a1 = 1;
int _a2 = 2;
// 静态成员变量 -- 属于类,属于类的每个对象共享,存储在静态区,生命周期是全局的
static int _scount;
// 静态成员变量不能通过缺省值初始化,因为静态成员没有初始化列表
// static int _scount = 0; //错误做法
};
4. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问 ,但是要注意一些问题:
虽然可以通过对象访问静态成员,但一般不推荐这样做,因为静态成员不属于任何对象。此外,如果静态成员是私有的,则不能直接从类或对象外部访问它,需要通过公有成员函数来访问。
代码例子(错误示范):
int main() {
// _scount 是私有的
// 下面的代码会报错,不能突破私有权限访问静态成员变量
cout << A::_scount << endl;
A a;
cout << a::_scount << endl;
return 0;
}
正确访问方式:
class A {
public:
static int GetCount() {
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
int main() {
A a;
cout << "Count: " << A::GetCount() << endl; // 正确访问
cout << "Count: " << a.GetCount() << endl; // 也可以通过对象访问,但不推荐
return 0;
}
5. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员 ,但是非静态成员函数可以调用类的静态成员函数。
class A {
public:
static int GetCount() {
// 不能访问非静态成员
// _a1++; // 错误
return _scount;
}
void NonStaticFunc() {
// 非静态成员函数可以调用静态成员函数
GetCount();
}
private:
int _a1 = 1;
static int _scount;
};
int A::_scount = 0;
int main() {
A a;
a.NonStaticFunc(); // 调用非静态成员函数,内部调用静态成员函数
return 0;
}
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。 友元分为:友元函数和友元类友元函数
前面我们讲过流插入和流提取运算符的重载,就用到了友元C++----类与对象(中篇)-CSDN博客。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。- 友元关系是单向的,不具有交换性:比如在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递:如果B是A的友元,C是B的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
class Time{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成
员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second){
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
内部类
概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。注意
内部类就是外部类的友元类(参见友元类的定义),内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元,不可以直接访问内部类的私有属性的成员。
特性
1. 内部类可以定义在外部类的public、protected、private。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
class A{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK:直接访问外部类的静态成员变量
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;//实例化内部类B对象的写法
b.foo(A());
return 0;
}
3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK:直接访问外部类的静态成员变量
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) <<endl;
return 0;
}
最终结果会输出4,我们来分析一下:
首先,需要明确的是,sizeof(A)
所计算的是类A的实例在内存中所占的大小,而非类A本身作为一个类型的大小。在类A中,static int k
是一个静态成员变量。静态成员变量是属于类的,而不是类的实例的。因此,它不会占用类实例的内存空间。所有类的实例共享同一个静态成员变量的值。int h;
是一个非静态成员变量。它属于类的每个实例,并且会占用内存空间。B
是A
的一个嵌套类,但是它没有存在外部类A中,也没有在外部类A中创建内部类的对象,所以也不会增加A的实例的大小。唯一占用空间的是h
,它是一个int
类型,占用4个字节。
匿名对象
概念
匿名对象是在表达式中临时创建的对象,没有名字,通常用于简化代码或直接在需要对象的地方进行一次性使用。
class A{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main(){
A aa(1); // 有名对象
A(2); // 匿名对象
return 0;
}
特征
1.调用成员函数的格式
#include<iostream>
using namespace std;
class Solution {
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl;
//...
return n;
}
};
int main(){
Solution sl;
sl.Sum_Solution(10);//有名对象调用函数的写法
Solution().Sum_Solution(20);//匿名对象调用函数的写法
return 0;
}
2.即用即销毁
class A{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main(){
A aa(1); // 有名对象--有名对象的生命周期在当前函数局部域
A(2); // 匿名对象--匿名对象的生命周期在所在当前行的表达式
return 0;
}
3.匿名对象具有常性
#include<iostream>
using namespace std;
class A{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main(){
A aa(1); // 有名对象 -- 生命周期在当前函数局部域
A(2); // 匿名对象 -- 生命周期在所在当前行的表达式
//A& ra = A(1); // 错误写法
const A& ra = A(1); //正确写法:匿名对象具有常性
return 0;
}
4.const引用可以延长匿名对象的生命周期
对于上一段代码,也许你会觉得,由于匿名对象的生命周期在当前行的表达式,所以ra在下一行就有变成类似于“野引用”的风险。但是const引用可以延长匿名对象的生命周期,生命周期延长至在当前函数局部域。
#include<iostream>
using namespace std;
class A{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main(){
A aa(1); // 有名对象 -- 生命周期在当前函数局部域
A(2); // 匿名对象 -- 生命周期在所在当前行的表达式
const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域
A(10);
return 0;
}
应用:C++ 编译器优化
在C++中,编译器会对临时对象的构造和拷贝进行优化,以减少不必要的开销。
以该代码为例:
#include<iostream>
using namespace std;
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)
{
}
A Func2() {
A aa; // 构造一个A对象
return aa; // 返回值优化:一般会调用拷贝构造
}
1. 拷贝构造+拷贝构造——>一次构造(返回值优化)
当函数返回一个对象时,编译器可能会优化掉返回过程中的拷贝构造,直接构造返回的对象在目标位置。
int main() {
cout << "在同一行同一个表达式时:" << endl;
A ra1 = Func2(); // 拷贝构造+拷贝构造 ->优化为拷贝构造
// 输出可能只显示一次构造和一次析构
cout << "不在同一行同一个表达式时:" << endl;
A a1;
a1 = Func2();
return 0;
}
输出:
2. 构造+拷贝构造——>优化为一次构造
在同一行或同一个表达式中,如果发生构造和拷贝构造,编译器可能会优化为只进行一次构造。
int main() {
cout << "不会优化,因为aa1是一个已存在的对象,需要被拷贝到Func1的参数中:" << endl;
A aa1;
Func1(aa1);
// 但是:
cout << "构造+拷贝构造 -> 优化为构造(临时对象直接构造在Func1的参数位置):" << endl;
Func1(A(1));
cout << "同上,构造一个A对象时,可能会直接构造在Func1的参数位置:" << endl;
Func1(1);
cout << "构造+拷贝构造 -> 优化为构造(直接构造aa2):" << endl;
A aa2 = 1;
return 0;
}
输出: