👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
- 一、再谈构造函数
- 1.1 构造函数体赋值
- 1.2 初始化列表
- 1.3 为什么C++要设计初始化列表
- 1. 若类中的成员变量包含`const`类型,必须在初始化列表位置进行初始化
- 2. 若类中的成员变量包含`引用`类型,必须在初始化列表位置进行初始化
- 3. 当成员变量是自定义类型,且该类没有默认构造函数时,必须在初始化列表位置进行初始化
- 1.4 扫尾补充
- 二、explicit关键字
- 三、static成员
- 3.1 概念
- 3.2 特性
- 3.3 面试题
- 3.4 使用静态成员变量的好处
一、再谈构造函数
1.1 构造函数体赋值
以下代码是在创建对象时,编译器通过构造函数,给对象中各个成员变量一个合适的初始值。
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
Year = year;
Month = month;
Day = day;
}
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2023, 1, 1);
d1.Print();
return 0;
}
【程序结果】
若对象调用了以上的构造函数,则对象就有了一个初始值。但这不能称其为对象成员中成员变量的初始值。因为初始化只能初始化一次,而构造函数体内可以多次赋值。因此,构造函数体中的语句只能叫做赋值,而不是初始化。
1.2 初始化列表
那成员变量该如何初始化呢?我们一起来看看下面的代码:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2023, int month = 5, int day = 23)
:Year(year)
,Month(month)
,Day(day)
{
}
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
【程序结果】
语法:以冒号开始,接着以逗号分割数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
1.3 为什么C++要设计初始化列表
有的人想,构造函数赋值好像就可以满足平时的代码需求。但对于某些数据类型,只能在初始化时进行赋值。
1. 若类中的成员变量包含const
类型,必须在初始化列表位置进行初始化
【在构造函数体内给值的情况】
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
{
i = x;
}
private:
const int i;
};
int main()
{
A aa1(2);
return 0;
}
【错误报告】
【正确做法:在初始化列表初始化】
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
:i(x)
{
}
void Print()
{
cout << i << endl;
}
private:
const int i;
};
int main()
{
A aa1(3);
aa1.Print();
return 0;
}
【程序结果】
2. 若类中的成员变量包含引用
类型,必须在初始化列表位置进行初始化
【在构造函数体内给值的情况】
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
{
i = x;
}
void Print()
{
cout << i << endl;
}
private:
int& i;
};
int main()
{
A aa1(3);
aa1.Print();
return 0;
}
【错误报告】
【正确做法:在初始化列表初始化】
#include <iostream>
using namespace std;
class A
{
public:
A(int &x)
:i(x)
{
}
void Print()
{
cout << i << endl;
}
private:
int& i;
};
int main()
{
int a = 3;
A aa1(a);
aa1.Print();
return 0;
}
【程序结果】
【补充】
- 为什么
const
和引用
类型需要在初始化列表给值?
原因如下:
引用和const
的特征:必须在定义的时候初始化。 因为const
修饰的变量在定义后不能被修改;同样的,引用在定义时,必须初始化,并且一旦引用一个变量,就再也不能引用其他变量。又因为构造函数体内可以多次赋值,因此导致报错。- 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次),当然也可以不初始化
【错误展示】
3. 当成员变量是自定义类型,且该类没有默认构造函数时,必须在初始化列表位置进行初始化
默认构造函数:无参构造、全缺省构造、编译器自动生成的构造函数(自身不定义的情况)
【错误展示】
#include <iostream>
using namespace std;
class A
{
public:
A(int c) // 不是默认构造函数
{
x = c;
}
private:
int x;
};
class B
{
public:
B(int i = 3)
:x(i)
{
}
private:
A a;
int x;
};
【错误报告】
【正确做法:初始化列表给值】
#include <iostream>
using namespace std;
class A
{
public:
A(int c)
:d(c)
{
}
private:
int d;
};
class B
{
public:
B(int i = 3)
:x(i)
,a(22)
{}
private:
A a;
int x;
};
int main()
{
B b1;
return 0;
}
【结果】
1.4 扫尾补充
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。但需要注意的是:初始化列表并不能百分之百完成所有初始化工作。例如:
#include <iostream>
using namespace std;
class Stack
{
public:
Stack(int default_capacity = 4)
:a((int*)malloc(sizeof(int) * default_capacity))
,top(0)
,capacity(default_capacity)
{
// 断言
if (a == nullptr)
{
return;
}
// 初始化
memset(a, 0, sizeof(int) * capacity);
}
private:
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
return 0;
}
【结果展示】
- 注意:初始化列表的初始化顺序一定是根据成员变量在类中声明顺序而定的
看看一下程序,就是因为没有根据成员变量在类中声明顺序,导致出现随机值
using namespace std;
class A
{
public:
A(int a)
:a1(a)
, a2(a1)
{}
void Print()
{
cout << a1 << " " << a2 << endl;
}
private:
int a2;
int a1;
};
int main()
{
A a(1);
a.Print();
return 0;
}
【解释 + 结果】
二、explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
#include <iostream>
using namespace std;
class A
{
public:
A(int x)
:a(x)
{
}
void Print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A a1(1); // 调用构造
A a2 = 2; // 隐式类型转化
a1.Print();
a2.Print();
return 0;
}
【结果展示】
A a1(1)
毋庸置疑调用的是构造函数,而对于A a2 = 2
单个参数是具有 类型转换 的作用。其隐式转化过程:用2
去调用构造函数生成一个A
类型的临时变量,临时变量再通过拷贝构造给a2
但需要注意的是:对于这种连续的构造,编译器会直接优化用直接构造。代码说话:
#include <iostream>
using namespace std;
class A
{
public:
// 构造函数
A(int x)
:a(x)
{
cout << "调用了构造函数" << endl;
}
// 拷贝构造函数
A(const A& d)
:a(d.a)
{
cout << "调用了拷贝构造函数" << endl;
}
void Print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A a2 = 2; // 隐式类型转化
a2.Print();
return 0;
}
【程序结果】
若不想有这样的隐式转化,可以在构造函数前加上explicit
,这样编译器就不支持隐式转化了
三、static成员
3.1 概念
声明为
static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量;用static
修饰的成员函数,称之为静态成员函数。
3.2 特性
- 要注意区分成员变量和静态成员变量。成员变量属于每一个类对象,存储在对象里;而静态成员变量属于类,属于类的每个对象共享,不属于某个具体的对象,存储在静态区。
- 静态成员变量必须在类外定义,(是不受
public
、protected
、private
访问限定符的限制)。定义时不添加static关键字,类中只是声明
- 类外定义的原因:
静态成员变量属于类,而不属于每一个类对象。因此,它们在内存中只有一份副本,不会随着类的对象的创建和销毁而变化。当我们在类定义中声明一个静态成员变量时,它只是一个声明,它并没有在内存中分配存储空间。因此,我们必须在类外部的某个地方为其分配存储空间,这样才能让它真正存在于内存中。因此,静态成员变量必须在类外定义,这样编译器才知道要为它分配存储空间。同时,我们也可以在类外初始化这个静态成员变量。
- 静态成员也是类的成员,受
public
、protected
、private
访问限定符的限制。因此,类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,指定类域和访问限定符就可以访问静态成员变量和静态成员函数
- 为什么静态成员函数没有隐藏的this指针?
静态成员函数是属于类的,而不是属于类的某个对象,因此在静态成员函数中没有隐含的this
指针。this
指针是指向当前对象的指针,因此只能在非静态成员函数中使用。而静态成员函数不依赖于具体的对象,只依赖于类本身,所以无需this
指针。在静态成员函数中,只能访问静态成员变量和静态成员函数,不能访问非静态成员变量和非静态成员函数。- 静态成员函数可以调用非静态成员函数吗?
不可以。调用非静态的成员函数需要this
指针,而静态成员函数没有隐藏的this
指针- 非静态成员函数可以调用类的静态成员函数吗?
可以。因为调用静态成员函数不需要this
指针
3.3 面试题
根据以上static
成员的概念和特征,我们来做一个经典面试题
- 实现一个类,计算程序中创建出了多少个类对象
#include <iostream>
using namespace std;
class A
{
public:
// 构造函数
// 每次创建对象时自增 count
A()
{
count++;
}
// 拷贝构造函数
// 每次创建对象时自增 count
A(const A& x)
{
count++;
}
// 析构函数
// 每次销毁对象时自减 count
~A()
{
count--;
}
//指定类域和访问限定符
//就可以访问静态成员变量和静态成员函数
static int GetACount()
{
return count;
}
private:
// 静态成员变量
static int count;
};
// 静态成员变量必须在类外定义,
// 定义时不添加static关键字
int A::count = 0;
int main()
{
// 如果直接想访问类中的count是不行的
// 因此我们首先想到成员函数
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
- 在类中,定义了一个静态成员变量
count
,用于统计当前类对象的数量。每次创建对象时,构造函数和拷贝构造函数会自增count
,每次销毁对象时,析构函数会自减count
。- 在主函数中,我们创建了三个对象
a1
、a2
、a3
,并输出当前对象数量。第一次输出并没有创建对象,因此输出0
;第二次分别创建了a1
和a2
,这两个都调用了构造函数,此时count = 2
,接着又通过a1
拷贝构造a3
,count
再自增1
,所以第二次count
为3
【结果展示】
3.4 使用静态成员变量的好处
-
全局性:静态成员变量是属于类的,而不是属于类的某个对象。因此,它可以被所有类的对象共享,具有全局性。
-
生命周期长:静态成员变量在程序运行期间只会被创建一次,它的生命周期长,可以一直存在于内存中,直到程序结束。
-
方便访问:由于静态成员变量是属于类的,因此可以通过类名直接访问,不需要先创建类的对象。
-
数据共享:静态成员变量可以用于实现数据共享,多个对象可以共享同一个静态成员变量,达到节省内存空间的目的。
-
保护数据:静态成员变量可以被用于保护数据,将数据设为私有的静态成员变量,只能通过类的公共接口来访问,从而保护数据的安全性。