总言
类和对象入门:介绍了初始化列表(说明其用法、必须使用初始化列表的成员、成员变量初始化顺序);explicit关键字与单参数隐式类型转换;匿名对象;static静态成员;友元类和友元函数;内部类等。
文章目录
1、初始化列表
基本概念与用法:
🌂解释一下什么是初始化列表?
🌂初始化列表在构造函数中的作用是什么?为什么我们需要使用初始化列表来初始化成员变量?
初始化列表与赋值:
🌂初始化列表和直接在构造函数体内赋值有什么区别?
🌂哪些情况下使用初始化列表是更优的选择?
初始化列表与const成员变量:
🌂为什么const成员变量必须在初始化列表中初始化?
🌂如果const成员变量没有在初始化列表中初始化,会发生什么?
初始化列表与继承:
🌂在派生类中,如何使用初始化列表来初始化基类的成员变量?
🌂如果派生类的构造函数没有显式调用基类的构造函数,会发生什么?
初始化列表的高级用法:
🌂你了解委托构造函数吗?能否给出一个使用委托构造函数和初始化列表的例子?
🌂你是否知道如何在初始化列表中调用虚函数?这会带来什么效果?
1.1、是什么和为什么
1.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、对日期类对象,在构造函数内赋值或使用列表初始化相差不大,但有些类的成员就必须使用初始化列表进行初始化。
②使用初始化列表赋值:
初始化列表基本语法: 以一个冒号(:
)开始,接着是一个以逗号(,
)分隔的数据成员列表,每个"成员变量"后面跟一个放在括号()
中的初始值或表达式。
以下为演示:
class Date
{
public:
Date(int year, int month, int day)
:_year(year) //初始化列表
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
注意事项:初始化列表是成员定义的地方,每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
1.1.2、问题扩展
1、使用初始化列表来初始化成员变量的原因(可以从以下几点考虑)
保证初始化顺序: 初始化列表确保成员变量按照它们在类中声明的顺序进行初始化,而不是按照它们在初始化列表中出现的顺序。这对于依赖初始化顺序的成员变量来说非常重要。(具体见下述1.3)
提高效率: 对于自定义类型的成员变量,使用初始化列表可以避免额外的拷贝操作或调用默认构造函数后再进行赋值,从而提高性能。
必要性: 对于常量成员变量和引用成员变量,必须在创建对象时通过初始化列表进行初始化,因为它们不能在对象创建后进行修改。
一致性: 使用初始化列表进行初始化可以使代码更加一致和可维护,因为它将初始化逻辑集中在一个地方,而不是分散在构造函数的多个地方。
2、“使用初始化列表”和“直接在构造函数体内赋值”,二者在C++中都可以用来初始化成员变量,但它们之间存在一些重要的区别:(可以从以下几点考虑)
初始化时机:
①初始化列表在构造函数体执行之前进行初始化。
②直接在构造函数体内赋值是在构造函数体执行时进行的。
性能:
使用初始化列表可以避免不必要的拷贝构造或赋值操作,尤其是在成员变量是自定义类型时。(详细解释见下述1.2.2)
对于内置类型(如int, float等),两者的性能差异可能不太明显,但对于复杂类型(如类、结构体等),初始化列表可能更优。
适用场景:
初始化列表适用于常量成员变量、引用成员变量、没有默认构造函数的成员变量。
直接在构造函数体内赋值适用于所有类型的成员变量,但可能不是最优选择。
初始化顺序:
初始化列表确保成员变量按照它们在类中声明的顺序进行初始化。
直接在构造函数体内赋值则不保证这种顺序。
1.2、必须使用初始化列表的成员
1.2.1、总述
类中包含以下成员,必须放在初始化列表位置进行初始化:
1、引用成员变量
2、const成员变量
3、自定义类型成员(且该类没有默认构造函数时)
1.2.2、为什么自定义类型成员必须在初始化列表初始化
对自定义类型成员,若没有默认构造函数,则其初始化需要调用初始化列表。若有默认构造函数,即使在构造函数内部初始化,仍旧会先被初始化列表初始化一次。(即使我们没有手动写初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。)
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
private:
int _year;
Time _t;
};
1.2.3、为什么引用、const类型成员必须在初始化列表初始化
1)、解释引用必须在初始化列表初始化的原因
回答: 引用必须在定义时初始化(C++入门篇,引用的特性),而初始化列表是成员定义的地方。
避免悬挂引用: 如果引用没有被初始化,它将不指向任何有效的内存位置。这样的引用被称为“悬挂引用”,尝试访问或操作它可能会导致不可预测的行为,甚至程序崩溃。因此,为了避免这种情况,C++要求引用在定义时必须被初始化。
2)、解释const成员必须在初始化列表初始化的原因
回答: const成员必须在定义的地方初始化,因为只有一次初始化机会(基于const定义的变量是不能被修改的),对于const成员,其初始化是在构造函数中的初始化列表出。
3)、以下为演示例子
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)//X加&则是实参y的引用,若不加&则,x的改变不会影响y。
:_year(year)
,_t(hour)
,_N(10)
,_ref(x)//ref是x的引用
{
_ref++;
}
private:
int _year ;
Time _t;
const int _N;//const成员,必须在初始化列表初始化
int& _ref;
};
int main()
{
int y = 0;
Date d(2022, 1, y);
return 0;
}
对引用的细节解释:此处传递y
变量给x
,若x
处的参数不加&
,则x
只是y
的一个临时拷贝,_ref
自增能影响x
变量但不能影响y
变量。加了&
后,_ref
的改变能影响y
的改变。
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
1.2.4、C++11为构造函数打下的补丁
C++11在默认构造函数中为内置类型打下的补丁,即私有成员声明处的缺省值,实际上它是作用于初始化列表的。
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
:_year(year)
, _t(hour)
,_N(10)
, _ref(x)
{
_ref++;
}
private:
// 声明
int _year = 0; // C++11 缺省值-- 初始化时没有显示给值就会用这个缺省值
Time _t;
const int _N;
int& _ref;
};
1.2.5、为什么不统一使用初始化列表?(一个举例)
1、语法设计的循序渐进性。
2、有些内容还是需要在函数体内初始化才方便。
class A
{
public:
A(int N)
:_a((int*)malloc(sizeof(int)* N))//1、哪怕我们在初始化列表初始化
, _N(N)
{
if (_a == NULL)//2、在函数体内还需要检查
{
perror("malloc fail");
}
memset(_a, 0, sizeof(int) * N);
}
// 3、倒不如直接在函数体内完成
A(int N)
:_N(N)
{
_a = (int*)malloc(sizeof(int) * N);
if (_a == NULL)
{
perror("malloc fail");
}
memset(_a, 0, sizeof(int) * N);
}
private:
// 声明
int* _a;
int _N;
};
1.3、成员初始化的顺序说明(C++中的一个坑)
演示代码:以下代码输出结果如何?
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
class A
{
public:
A(int a)
// 成员变量定义
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
// 成员变量声明
int _a2;
int _a1;
};
int main() {
// 对象定义
A aa1(1);
aa1.Print();
}
演示结果如下:
结论:成员变量在类中的声明次序 ,就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
ps:对象有很多对象,成员列表是针对这些每个对象的单独定义。
1.4、explicit关键字与单参数构造中的隐式类型转换
1.4.1、单参数的构造函数支持隐式类型转换
1)、基本介绍
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
单参数的构造函数支持隐式类型转换: 这是指,当尝试用一个类型的值去初始化另一个类型的对象时,如果后者有一个接受该类型参数的构造函数,那么编译器会自动调用这个构造函数来完成类型转换。这种转换是隐式的,意味着程序员不需要显式地指定转换操作。
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year):"<<_year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
int main() {
// 直接调用构造的写法:
Date d1(2021);
// 隐式类型转换:其效果等同于上述Date d2(2022);
//构造 + 拷贝构造 -> 编译器优化 = 直接调用构造
Date d2 = 2022;
//原本2022先构造一个Date tmp(2022),再用tmp拷贝构造d2
//但经过编译器优化,会直接调用构造
//就如普通变量int类型i转换为double时,中间会进过一个临时变量tmp,其具有常属性(const修饰)
//因此,此处若带有&,则需要加上const
const Date& d3 = 2023;
int i = 10;
const double& d = i;
}
关于隐式类型转换的再理解:(温故:常引用,const变量与隐式类型转换)
不是直接将 i
的值给 d
,它们中间会产生一个临时变量。如果此时变量d
是引用,则此处无法直接转换,原因是临时变量具有常属性,此处变量d
引用的是中间生成的那个临时变量,在d
没被const
修饰时,属于权限放大。
const Date& d3 = 2023;
int i = 10;
const double& d = i;
2)、该语法的意义体现
举例一 :在创建对象时
C++中有一个string类,我们以其为例子进行构造,如下:
int main()
{
string s1("insert");
string s2 = "insert";
}
string s1(“insert”)
:是一种常规写法
string s2 = "insert";
:这种写法也是可行的,且因与内置类型相似,在理解角度相对容易。能支持s2
这种写法的原因就在于上述讲述的,单参数的隐式类型转换。
举例二 :在引用传参时
有一个问题是:明明可以用s1
,为什么要多弄一个s2
的写法?
如下述代码,假设我们有一个函数,其参数是类类型,此时我们该如何传参?
void func(const string& s)
{}
方法一: 以string
类先构造一个具体的对象,为其赋相应值,再将该对象作为参数传入。
string str("abcde");
func(str);
方法二: 直接将常量字符串传入。这里就涉及到一个问题,为什么可以这样写?原因正是在于存在隐式类型转换。
func("abcde");
但这样子的写法需要注意函数参数类型,由于“abcde”
属于常量字符串,具有常属性,在引用做参数时,需要加const
修饰,即const string& s
,这个条件下才能正常传参。
void func1(string& s)
{}
void func2(string s)
{}
int main()
{
func1("abcde");//error:不加const修饰直接传参,属于权限放大,故而会报错。
func2("abcde");//right:这种情况,其属于传值传参,非指针非引用,不存在权限问题。
return 0;
}
1.4.2、介绍explict关键字
1)、问题引入
虽然隐式转换在某些情况下很方便,但它们也可能导致以下问题:
意外行为: 程序员可能没有意识到正在发生类型转换,从而导致逻辑错误或不易察觉的bug。
类型安全: 隐式转换可能会破坏类型安全,使得代码更难理解和维护。
性能开销: 虽然现代编译器通常会对隐式转换进行优化,但在某些情况下,这些转换可能会导致不必要的性能开销。
2)、explicit关键字
为了避免隐式类型转换带来的上述这些问题,C++提供了explicit
关键字,主要用于修饰类的构造函数,以防止构造函数发生隐式类型转换。 当在构造函数前加上explicit
关键字时,编译器将不会进行隐式类型转换。
如下所示:
class Date
{
public:
explicit Date(int year)
//Date(int year)
:_year(year)
{
cout << " Date(int year):"<<_year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
int main() {
// 直接调用构造
Date d1(2021);
// 隐式类型转换:此时这种写法是不被允许的
Date d2 = 2022;
const Date& d3 = 2023;
int i = 10;
const double& d = i;
}
可看到编译器报错,这是因为explict
关键字的作用是:防止类对象发生隐私类型转换。
1.5、匿名对象
1.5.1、基础介绍
在C++中,匿名对象(也称为临时对象)是没有显式名称的对象,它们在表达式中创建并立即使用,然后通常会被销毁。匿名对象通常用于需要临时存储数据但不希望这些数据具有持久性的场景。
PS:匿名对象在表达式结束时会被销毁,因此它们的生命周期很短。如果需要在多个表达式或函数之间共享数据,应该使用具有显式名称的对象或变量。
2)、使用举例
匿名对象的一个常见用途是在表达式中初始化一个对象,并立即将其用作另一个表达式的一部分。例如,当调用一个需要对象作为参数的函数时,可以创建一个匿名对象作为参数传递(以下演示)。
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year):" << _year << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << _year << endl;
}
~Date()
{
cout << "~Date()" << _year << endl;
}
private:
int _year;
};
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main() {
// 【基本介绍】
// 匿名对象,生命周期只有这一行
Date(2001);
// 有名对象,生命周期在整个作用域
Date d4(2004);
// 【匿名对象的一些使用场景】
//创建对象调用:
Solution slt;
slt.Sum_Solution(10);
//匿名对象调用:
Solution().Sum_Solution(10);
//若单纯只是为了调用一次对象中的函数,
//后续也不需要使用该对象
//就可以直接调用匿名对象
//而非为了调用该函数而先创建对象
}
1.5.2、判断题:(几次构造、几次析构)
1)、专题1:传值传参&&传引用传参,下列分别用了几次构造,几次拷贝构造?
代码一:
void f1(W w)//传值传参
{ }
int main()
{
W w1;
cout << endl;
f1(w1);//传值传参,会产生拷贝构造:一次构造+一次拷贝构造
cout << endl;
return 0;
}
代码二:
void f2(const W& w)//传引用传参
{}
int main()
{
W w1;
cout << endl;
f2(w1);//一次构造:因此建议加上const、&传引用传参
cout << endl<<endl;
return 0;
}
代码三:
void f1(W w)//传值传参
{}
int main()
{
W w1;
cout << endl;
f1(W()); // 匿名对象,本来构造+拷贝构造-->编译器的优化-->直接构造
//结论:连续一个表达式步骤中,连续构造一般都会优化 --> 合二为一
return 0;
}
对上述现象的再次解释:
这是C++编译器优化中的一个常见现象,即当在单个表达式中连续进行构造和拷贝构造(或移动构造)时,编译器可能会进行优化,以避免不必要的中间对象的创建和销毁。这种优化通常被称为“返回值优化”或“命名返回值优化”。
这种优化是编译器自动进行的,程序员通常不需要(也不应该)显式地依赖它。但了解这种优化有助于理解为什么在某些情况下,即使代码看起来应该进行多次构造和拷贝,实际运行时性能却可能非常好。
上述用于验证的类:
class W
{
public:
W() {
cout << "W():构造" << endl;
}
~W() {
cout << "~W():析构" << endl;
}
W(const W& w)
{
cout << "W(const W& w):拷贝构造" << endl;
}
};
2)、专题2:传值返回和传引用返回,下列分别用了几次构造,几次拷贝构造?
代码一:
W f3()//传值返回
{
W ret;//构造
return ret;//拷贝构造
}
int main()
{
f3();
return 0;
}
代码二:
W f3()//传值返回
{
W ret;//构造
return ret;//拷贝构造
}
int main()
{
//f3();//一次构造。一次拷贝构造。
W w1 = f3();//理论上:一次构造,两次拷贝构造。
return 0;
}
实际效果解释:
3)、例题进阶:
相关类:
class Widget
{
public:
Widget(int x = 0)
{
cout << "Widget()" << endl;
}
Widget(const Widget& w)
{
cout << "Widget(const Widget& w)" << endl;
}
Widget& operator=(const Widget& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~Widget()
{
cout << "~Widget()" << endl;
}
private:
};
问题:几次构造,几次拷贝构造?
例题一:
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
main() {
Widget x;
Widget y = f(x);
//Widget y = f(f(x));
}
例题二:
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
main() {
Widget x;
//Widget y = f(x);
Widget y = f(f(x));
}
2、Static,类的静态成员
基础理解:
🌂你能解释一下什么是类静态成员吗?
🌂静态成员变量和静态成员函数在类中有什么特殊之处?
🌂为什么需要静态成员?
声明与定义:
🌂如何在类声明中声明静态成员?
🌂静态成员的定义通常在哪里进行?
🌂静态成员的定义和声明有什么区别?
初始化与访问:
🌂如何初始化静态成员变量?
🌂静态成员变量可以在类的构造函数中初始化吗?
🌂静态成员可以通过类的对象访问吗?
生命周期和存储:
🌂静态成员的生命周期是怎样的?
🌂静态成员变量在内存中的存储位置是怎样的?
继承和派生:
🌂在派生类中,如何访问基类的静态成员?
🌂基类的静态成员在派生类中会被继承吗?
2.1、是什么和为什么
1)、为什么要让类有静态成员
静态成员提供了一种灵活而强大的机制,用于在C++类中实现共享数据、全局访问点、限制实例化、封装和抽象等功能。它们在某些情况下非常有用,可以帮助我们编写更加高效、可维护和可扩展的代码。
比如,我们会遇到这样的情况:实现一个类,计算程序中创建出了多少个类对象。(相关解答见后续11.2)
一般而言,若没有类静态成员,我们会使用全局变量来实现。但全局变量对所有对象都起作用,如果我们想要一个单独对对象起作用的变量,则就需要静态成员。
2)、静态成员是什么?
声明为static
的类成员称为类的静态成员:
①、用static修饰的成员变量,称之为静态成员变量;
②、用static修饰的成员函数,称之为静态成员函数。
PS:静态成员变量一定要在类外进行初始化。
以下为类中静态成员举例:此处先简单理解静态成员变量如何定义和初始化,该部分代码相关解答见后续。
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;//静态成员变量的初始化需要在类外进行。
//初始化变量时要带上域作用限定符,表明该成员变量出自哪。
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
两个问题:
1、能否在构造函数的初始化列表中为静态成员初始化?(NO)
2、能否在声明处为静态成员赋予缺省值?(NO)
回答:因为静态成员不能在类内初始化,所以在类中定义处进行缺省赋值也是不被允许的。(由初始化列表得知,C++11的该补丁在初始化列表中起作用)
private:
static int _scount = 0;//这种初始化方式是不被允许的。
2.2、类静态成员的特性(包含类静态成员声明、定义)
2.2.1、对静态成员变量
1、存储方式: 静态成员变量在内存静态区中只存储一份,而不是为每个对象实例存储一份。这意味着,无论创建了多少个对象实例,它们都共享同一个静态成员变量。
2、声明和定义: 类的静态成员变量在类中只能声明,受访问限定符限制。其定义和初始化要在类外进行(也有些编译器支持在类内定义并初始化,但大多数编译器不支持,所以,安全的做法还是类内声明,类外定义和初始化)。
class Myclass
{
private:
static int a; // 类内声明
}
int Myclass::a = 10; // 类外定义和初初化,注意,这时不需要带static,但仍旧需要带类域。
3、初始化: 静态成员变量不属于任何特定的对象实例,必须在类外部进行初始化。此外,静态成员变量不走初始化列表,其不支持给缺省值,静态成员变量只能初始化一次。
4、访问方式: 静态成员变量可以通过类名直接访问,也可以通过对象实例访问。由于静态成员变量与对象实例无关,通过对象实例访问静态成员变量并不是推荐的做法。
5、生命周期: 静态成员变量的生命周期与程序的运行时间相同,它们在整个程序执行期间都存在。
6、大小: 计算类的大小时,不包括静态成员变量的大小,因为它在静态区,不算在类里。
2.2.2、对静态成员函数
1、存储方式: 静态成员函数不占用类的实例内存空间,它们存储在全局/静态存储区。
2、声明和定义: 类的静态成员函数的声明和定义与普通的成员函数相同,既可以在类内声明和定义,也可以在类内声明类外定义。
class Myclass
{
public:
static int fun(); // 类内声明
}
int Myclass::fun() // 在类外的函数定义不能写static,但仍旧需要声明来自哪个类(类域)。
{
return 1;
}
3、调用方式: 静态成员函数可以通过类名直接调用,而不需要创建类的对象实例。这使得静态成员函数可以在没有对象实例的情况下被调用。
4、访问限制: 静态成员函数 只能访问静态成员变量和其他静态成员函数,不能访问类的非静态成员(即实例成员)。这是因为静态成员函数不与任何特定的对象实例关联,所以它没有this
指针来访问非静态成员。
5、生命周期: 静态成员函数在程序开始执行时就已经存在,并且在程序结束时销毁。
6、用途: 静态成员函数通常用于执行与类相关但不依赖于任何特定对象实例的任务。它们常用于实现工厂方法、单例模式、工具类等场景。
2.2.3、小结
2.3、类的静态成员访问方式
2.3.1、类的静态成员变量在公有情况下的访问方式?
在公有情况下,在类外访问静态成员,既可以使用类名,也可以使用对象。
如下述代码举例:
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
//private:将类成员置为公有
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
//此时的访问方式:
cout << a1._scount << endl;
cout << a2._scount << endl;//1、通过“实例对象 ”访问类的静态成员变量
cout << A::_scount << endl;//2、通过“类名 ”访问类的静态成员变量
}
int main()
{
TestA();
return 0;
}
细节理解:此处并非指在a1、a2对象中去寻找_scount
。编译器会在全局中找,然后再去指定的类中寻找。
cout << a1._scount << endl;
cout << a2._scount << endl; //对象.静态成员
cout << A::_scount << endl; //类名::静态成员
2.3.2、静态成员变量在类中声明,如被设为私有,如何访问获取?
访问方式一:
1、如示例代码:可以在类中定义一个GetCount
的函数,如此就可以在TestA
(类外)中使用对象去问了。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
int GetCount() //定义一个成员函数
{
return _scount; //返回私有对象值
}
private://类成员私有时
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
cout << a1.GetCount() << endl; //只能使用对象去调用
cout << a2.GetCount() << endl;
cout << a3.GetCount() << endl;
}
int main()
{
TestA();
return 0;
}
注意事项:访问时是用对象去调用。(即这种写法下不能使用类调用该成员函数)
访问方式二:
2、若不用对象去调用,则需要使用静态成员函数。如所示代码:
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetCount()
{
//_a = 1; //error
return _scount;
}
private:
int _a;
static int _scount;
};
int A::_scount = 0;
void TestA()
{
A a1;
A a2;
A a3(a2);
cout << a1.GetCount() << endl;//1、对象.静态成员
cout << a2.GetCount() << endl;
cout << A::GetCount() << endl;//2、类名::静态成员
}
int main()
{
TestA();
return 0;
}
注意事项:
①、由于静态成员函数没有this
指针,因此其不能访问非静态成员,只能调用静态成员。
②、既然是静态成员函数,说明其可以在类中定义。
2.3.3、cosnt修饰静态成员变量
const修饰的静态成员变量:保存在常量区,是只读的(该值在初始化后不能再被修改),并且只能初始化一次。该静态成员变量可以在类内定义且初始化,静态成员函数可以访问const
修饰的静态成员变量。
2.3.4、面试题:实现一个类,计算程序中创建出了多少个类对象?
根据上述学习,由于静态成员为所有类共享,这里可以设置静态成员变量进行统计,并将结果通过静态成员函数返回。
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;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}
2.4、应用举例一(以例题说明:1+2+3+……+n求和)
2.4.1、题目和思路
class Solution {
public:
int Sum_Solution(int n) {
}
};
解决方案:借助变长数组
整体思路: 对象实例化时会调用构造函数,假若我们定义一个以类为类型的数组,其长度根据我们需要求和的最大数值而定,那么我们就能得到1~N需要求和的数。此时再将其累加记录结果即可。
2.4.2、方法一:全局变量&&变长数组
//定义全局变量
int i=1;
int sum=0;
class Sum
{
public:
Sum()//Sum类的默认构造函数:无参数情况
{
sum+=i;//sum是用于累加这些数值的。
++i;//i是用于统计第几次对象实例化,以模拟实现1~100数值
}
};
class Solution {
public:
//此处将需要写的接口放到了类里
//Sum_Solution成员函数中的变量n即输入值
int Sum_Solution(int n) {
Sum a[n];//使用了变长数组,即数组元素有n个(n是变量),数组类型为Sum类(是一个类)。
//将Sum作为类,当其实例化时,每次都会自动调用对应的构造函数。
//实例化了n个Sum类的对象,构造函数在此处调用了n次。
return sum;//sum是一个全局变量。
}
};
2.4.3、方法二:静态类成员&&变长数组
使用全局的i
、sum
,容易被修改,因此相对不太提倡。因此此处使用了静态成员。
class Sum
{
public:
Sum()//Sum类的默认构造函数:无参数情况
{
_sum+=_i;
++_i;
}
static int GetSum()
{
return _sum;//由于将sum变量设置为了类的成员
//此处变为私有,要访问sum值,可在此处设置一个静态的成员函数
//PS:若不使用静态的成员函数,则调用sum时还需要创建一个对象
//从设计的角度使用静态成员函数比较方便。
}
private:
static int _sum;//静态成员变量的声明
static int _i;
};
//静态成员变量的定义和初始化
int Sum:: _sum=0;
int Sum::_i=1;
class Solution {
public:
//此处将需要写的接口放到了类里
//Sum_Solution成员函数中的变量n即输入值
int Sum_Solution(int n) {
Sum a[n];//使用了变长数组,即数组元素有n个(n是变量),数组类型为Sum类(是一个类)。
//将Sum作为类,当其实例化时,每次都会自动调用对应的构造函数。
//实例化了n个Sum类的对象,构造函数在此处调用了n次。
return Sum::GetSum();
}
};
2.4.4、方法三:内部类(本文后续的知识)&&变长数组
class Solution {
class Sum//内部类
{
public:
Sum()
{
_sum += _i;
++_i;
}
};
public:
int Sum_Solution(int n) {
Sum a[n];
return _sum;
}
private:
static int _sum;//静态成员变量的声明
static int _i;
};
//静态成员变量的定义和初始化
int Solution::_sum = 0;
int Solution::_i = 1;
2.5、应用举例二:如何设计一个只能在栈上定义的对象?
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so; //此处使用了传值返回。
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{}
private:
int _x = 0;
int _y = 0;
};
int main()
{
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
3、友元(friend)
3.1、友元总述
友元分为:友元函数和友元类。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
基本概念:
🌂你能解释一下什么是C++中的友元(friend)吗?
🌂友元函数和友元类有什么区别?
友元函数:
🌂为什么需要友元函数?
🌂友元函数如何访问类的私有和保护成员?
🌂友元函数是类的成员函数吗?
🌂能否让友元函数同时访问多个类的私有成员?
友元类:
🌂什么是友元类?
🌂友元类如何访问另一个类的私有和保护成员?
🌂友元关系是否具有传递性?
🌂友元关系是否可以在派生类中被继承?
使用场景与限制:
🌂你能给出一些使用友元的实际场景吗?
🌂使用友元有哪些潜在的风险或限制?
🌂过度使用友元会导致哪些问题?
友元与封装:
🌂友元如何影响类的封装性?
🌂在设计类时,应如何权衡封装和友元的使用?
3.2、友元函数
1)、回顾日期类中流插入流提取的实现:
在类和对象(三)中,实现日期类的流插入和流提取时,我们发现发现没办法将operator<<
重载成成员函数。因为cout
的输出流对象和隐含的this
指针在抢占第一个参数的位置。类中this
指针默认是第一个参数(左操作数),实际使用时cout
也需要是第一个形参对象,才符合日常写法,二者存在冲突。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// 若在类中实现,则传参使用时为:d1 << cout;
// 等价于 d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
因此,在实际实现时,我们将operator<<
重载成全局函数来使用,此时面临一个问题:类外没办法访问非公有成员,此时就需要友元来解决(operator>>
同理)。
详细介绍可查看博文:日期类的模拟实现·流插入和流提取
class Date
{
//声明:类中,使用关键字friend进行声明
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//定义:类外,是友元函数的定义
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
2)、友元函数的几个特性
1、访问权限: 友元函数可以访问类的私有(private
)和保护(protected
)成员。 这是友元函数最显著的特性,它使得友元函数能够访问类的内部数据,即使这些数据通常对类的外部是不可见的。
2、非成员函数: 尽管友元函数可以访问类的私有和保护成员,但它并 不是类的成员函数,而是定义在类外部的普通函数。这意味着友元函数没有类的this
指针,不能直接调用类的非静态成员函数,也不能用const
修饰(const
修饰this
指针指向的内容,友元函数作为全局函数没有this
指针。)。
这里有一个需要注意的点: 虽然友元函数不能直接调用非静态成员函数,但它可以通过显式地传递对象实例作为参数来间接调用非静态成员函数。
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
friend void printValue(MyClass& obj);
void showValue() {
std::cout << "Value: " << value << std::endl;
}
};
void printValue(MyClass& obj) {
// 虽然printValue是友元函数,但它不能直接调用showValue
// 但是,它可以通过传递的对象实例来调用
obj.showValue();
}
3、声明位置: 友元函数可以在类定义的任何位置声明,不受类访问限定符的限制(如public
、protected
、private
)。
4、多重友元: 一个函数可以是多个类的友元函数。这意味着同一个函数可以访问多个类的私有和保护成员。
5、调用方式: 友元函数的调用与普通函数的调用方式和原理相同。你不需要创建类的对象实例就可以调用友元函数,除非你正在访问类的实例成员。
6、提高效率: 通过允许友元函数访问类的私有和保护成员,友元函数可以提高程序运行效率,因为它可以直接访问和操作类的内部数据,而不需要通过类的公共接口。
7、破坏封装性: 然而,友元函数也破坏了类的封装性和隐藏性。封装性是面向对象编程的一个重要特性,它隐藏了类的内部实现细节,只通过公共接口与外部交互。友元函数的存在使得类的内部数据可以被外部直接访问和修改,这可能会破坏封装性。
8、友元关系的单向性和非传递性: 友元关系是单向的,不具有交换性。也就是说,如果B
是A
的友元,那么B
可以访问A
的私有成员,但A
不能访问B
的私有成员。此外,友元关系也不能传递。如果B
是A
的友元,C
是B
的友元,这并不意味着C
是A
的友元。
3.3、友元类
C++中的友元类是一种特殊的关系,允许一个类访问另一个类的私有和保护成员。 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、声明方式: 声明一个 类B
是 另一个类A
的 友元类 , 可以 在 类A
中使用 friend
关键字对类B
进行声明。
如下述Time
类和Date
类,在Time
类中声明Date
类为其友元类,那么可以在Date
类中直接访问Time
类的私有成员变量。
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)
{
// 可以在Date类中直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
// 这不仅仅是针对友元类的某个特定成员函数,而是Date类的所有成员函数都可以算是Time类的友元函数。
private:
int _year;
int _month;
int _day;
Time _t;
};
2、单向性: 友元关系是单向的,不具有交换性。如果类B
是类A
的友元类,那么类B
可以访问类A
的私有成员,但类A
不能自动访问类B
的私有成员。除非类A
也被声明为类B
的友元类。
示例:上述例子中,Date
类为Time
类友元类,那么可以在Date
类中直接访问Time
类的私有成员变量,但想在Time
类中访问Date
类中私有的成员变量则不行。
3、非传递性: 友元关系不具有传递性。如果类B
是类A
的友元类,类C
是类B
的友元类,这并不意味着类C
自动成为类A
的友元类。每个友元关系都需要单独声明。
4、友元关系不能继承: 这意味着,如果一个类(我们称之为基类)声明了另一个类(我们称之为友元类)为其友元,那么基类的派生类不会自动继承这种友元关系。
4、内部类
1)、内部类的概念和特性
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
1、内部类是一个独立的类,它不属于外部类,不能通过外部类的对象去访问内部类的成员。但内部类使用时,受外部类的类域限制,需要用相应访问限定符。
2、内部类是外部类的友元类,参见友元类的定义,内部类可以无条件地访问外部类的所有成员(包括私有和保护成员)。
以下为代码演示:
class A
{
private:
static int k;
int h;
public:
//B定义在A的里面
//1、内部类(B)受外部类(A)的类域限制,需要用相应访问限定符
//2、内部类(B)天生是外部类(A)的友元,可以访问外部类的私有成员
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK:A的静态成员可以直接被B访问而不用类名
cout << a.h << endl;//OK:B类可以使用A的私有成员
}
};
};
int A::k = 1;
int main()
{
A::B b;//访问B类受到A的类域限制
b.foo(A());
return 0;
}
特性:
1、定义位置: 内部类可以定义在外部类的public
、protected
、private
部分中。这意味着内部类的访问权限取决于它在外部类中的定义位置。
如果内部类定义在public,则可通过 【外部类名::内部类名】 来定义内部类的对象。
如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。
2、访问外部类成员: 内部类可以直接访问外部类中的static
和枚举
成员,不需要外部类的对象或类名。这一点非常有用,因为它允许内部类与外部类共享某些静态数据或常量。
3、大小无关: sizeof(外部类)
的计算与外部类本身的大小有关,与内部类没有任何关系。这意味着内部类的大小不会影响外部类的大小。
4、继承: 内部类可以继承外部类或其他类。这使得内部类可以重用外部类的代码,并添加自己的特定功能。
5、自测
5.1、题一:必须使用初始化列表初始化的成员
有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:( )
class A{
private:
int a;
public:
const int b;
float*& c;
static const char* d;
static double* e;
};
b、c
5.2、题二:初始化列表顺序
下面程序的运行结果是( )?
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值 (正确)
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();
}
5.3、题三:static关键字修饰局部变量、全局变量、函数
在一个cpp文件里面,定义了一个static类型的全局变量,下面一个正确的描述是( )
A.只能在该cpp所在的编译模块中使用该变量
B.该变量的值是不可改变的
C.该变量不能在类的成员函数中引用
D.这种变量只能是基本类型(如int,char)不能是C++类型
static
关键字介绍,详细看博文:初识C语言
A.正确,static限制了变量具有文件域
5.4、题四:static类成员
关于C++类中static 成员和对象成员的说法正确的是( )
A.static 成员变量在对象构造时生成
B.static 成员函数在对象成员函数中无法调用
C.static 成员函数没有this指针
D.static 成员函数不能访问static 成员变量
正确选C。
解释A:static成员变量在对象生成之前生成
5.5、题五:隐含的this指针
下面程序段包含4个函数,其中具有隐含this指针的是( )
int f1() {};
class T
{
public:
static int f2();
private:
friend int f3();
protected:
int f4();
};
正确选f4。
f1:全局函数不具备this指针
f2:static函数不具备this指针
f3:友元函数不具备this指针
f4:普通成员方法具有隐藏的this指针
5.6、题六:友元函数与类成员函数
下面有关友元函数与成员函数的区别,描述错误的是?( )
A.友元函数不是类的成员函数,和普通全局函数的调用没有区别
B.友元函数和类的成员函数都可以访问类的私有成员变量或者是成员函数
C.类的成员函数是属于类的,调用的时候是通过指针this调用的
D.友元函数是有关键字friend修饰,调用的时候也是通过指针this调用的
D。