摘要
本文主要介绍类和对象基础知识点,从类的创建到销毁,涵盖类的完整生命周期与各种使用细节,并配备许多练习提问。举例清晰,图文并茂,通俗易懂,适合初学者学习与回顾复习。
第一章:类的定义与实例化
一、类的定义
类的两种定义方式
(1)声明和定义全部放在类体中。
class Date
{
public:
Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void print_date()//打印成员变量
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
(内联函数是C++中的一种特殊函数,编译器会在调用处直接展开其代码,而不建立栈帧进行常规函数调用,以提高执行效率。)
(2)类声明放在.h文件中,成员函数定义放在.cpp文件中。
//Date.h
class Date
{
public:
Date(int year, int month, int day);//构造函数
void print_date();//打印成员变量
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include"Date.h"
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::print_date()//打印成员变量
{
cout << _year << '-' << _month << '-' << _day << endl;
}
注意:成员函数名前需要加类名:: ,用来指明这是属于哪个类的成员函数。
长的函数应声明与定义分离,短小的可以直接定义在类里,默认为内联函数提高效率。
(在类外定义的成员函数不是内联函数)
二、类的访问限定符及封装
1.访问限定符
效果:
public修饰的成员在类外可以直接被访问,protected和private修饰的成员在类外不能直接被访问
(此处protected和private是类似的,具体差别体现在继承部分,此处不详细介绍)。
例如:我们可以在main函数中调用Date类中public修饰的成员函数,但不能访问private修饰的成员变量
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void print_date()//打印成员变量
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2024, 8, 8);
d.print_date();
//cout << d._year 直接访问私有成员变量会报编译错误
return 0;
}
正确运行结果截图:
直接访问_year变量错误信息:
作用域:
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
默认访问权限:
class的默认访问权限为private,struct的默认访问权限为public。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
原因:这样做在编译时就能完成相应的权限检查,避免了运行时的性能开销,并且符合C++语言设计的高效性原则。
问题:C++中struct和class的区别是什么?
答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
(在继承和模板参数列表位置,struct和class也有区别,这里不作详解)。
2.封装
面向对象的三大特性:
封装、继承、多态。
封装:
封装是面向对象编程的核心概念之一,指将数据和操作数据的方法(即函数)捆绑在一起,以实现对数据访问的控制和保护。通过封装,对象的内部细节被隐藏在对象外部不可见,外部只能通过对象提供的接口来访问和操作数据,从而提高了程序的安全性和可维护性。
例如:我们无法在类外直接访问Date中的私有成员变量,但可以访问公有成员函数在打印数据。
C++实现封装的方式:
在C++中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过设定访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类的作用域:
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
三、类的实例化
用类类型创建对象的过程,称为类的实例化。
(1)类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
(2)一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
我们可以理解为,定义一个类就是画好房屋的设计图纸,实例化就是依据图纸盖出实体房屋。
1.类对象模型
现有一个类A:
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
类对象的存储方式猜测(受不同环境和编译器影响):
方式一:对象中包含类的各个成员
缺陷:每个对象中存储的成员变量是不同的,但是却调用同一份成员函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份成员函数代码,相同代码保存多次,会浪费很多空间。那么如何解决呢?
方案二:成员函数代码只保存一份,在对象中保存存放代码的地址
在这个方案中,每个对象中只存储成员变量和一个类函数表地址,可以通过该地址找到类的成员函数进行调用。也可将成员函数统一存放在一个内存中的公共代码区,每个成员都可以在公共代码区中找到成员函数代码。
那么计算机采用的是哪种方式呢?
#include<iostream>
using namespace std;
// 类中既有成员变量,又有成员函数
class A1
{
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
cout << "A1大小:" << sizeof(A1) << endl;
cout << "A2大小:" << sizeof(A2) << endl;
cout << "A3大小:" << sizeof(A3) << endl;
return 0;
}
运行结果截图:
结论:一个类的大小,实际就是该类中“成员变量”之和,但要注意内存对齐!
注意空类的大小,空类比较特殊,编译器给了空类一个字节的空间来唯一标识这个类的对象。
2.结构体内存对齐规则
1.第一个成员在与结构体偏移量为0的地址处(结构体的起始位置)。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。(VS编译器中默认的对齐数为8。)
3.结构体总大小为最大对齐数(所有变量类型最大者与默认对齐参数取最小值)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举例:
#include<iostream>
using namespace std;
class A
{
private:
int _a;
char _b;
char _c;
};
int main()
{
cout << "A大小:" << sizeof(A) << endl;
return 0;
}
对于A这个类,存在一个int类型与两个char类型,按照规则,第一个int变量在偏移量0地址处占据4个字节;第二个char变量自身大小为1字节,默认对齐数为8,取较小值1,则char可以存放到偏移量为1的倍数4这个位置,占据1个字节空间;同理,第三个char变量存放到偏移量为5的位置,占据1个字节空间。
那么这个类总大小是多少呢?结构体总大小为最大对齐数(所有变量类型最大者与默认对齐参数取最小值)的整数倍。所有变量类型最大者应为4字节的int而非1字节的char,默认对齐数为8,二者取较小值为4,所以这个类的总大小只能为4的倍数(4,8,12,16…),此处应为8而非6。
代码运行结果:
问题:为什么要进行内存对齐?
答:
1.提高访问速度
现代计算机的处理器通常对内存访问有一定的要求,即希望数据能够按照特定的边界(如4字节、8字节等)进行对齐。如果数据没有对齐,处理器可能需要进行多次内存访问来读取或写入数据,从而降低访问速度。对齐可以确保大多数情况下数据都是在一个内存周期内完成读写操作,提高性能。
2.硬件要求
某些硬件架构严格要求数据必须按照特定的边界对齐,否则会引发硬件异常或错误。例如,一些处理器不允许从非对齐地址访问某些类型的数据,如果强行访问会导致程序崩溃。
3.兼容性和可移植性
内存对齐使得代码在不同硬件平台上更具可移植性,因为不同平台对于对齐的要求可能不同。通过遵守通用的对齐规则,可以确保代码在多种平台上都能正常运行。
4.简化内存管理
内存对齐可以简化内存管理,特别是在涉及到缓存行(cache line)时。对齐的数据更容易被加载到缓存中,提高缓存命中率,进而提升整体性能。
问题:如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
答:在Visual Studio编译器中,通过使用 #pragma pack 可以控制结构体的对齐方式,包括非标准的对齐方式(如3字节、5字节对齐)。
#pragma pack(push,1)// 保存当前对齐状态,并设置新的对齐方式为1字节对齐
struct Example1
{
char a;
int b;
short c;
};
#pragma pack(pop)// 恢复之前的对齐状态
#pragma pack(push,3)// 设置新的对齐方式为3字节对齐
struct Example2
{
char a;
int b;
short c;
};
#pragma pack(pop)// 该复之前的对齐状态
注意:
更改编译器默认对齐数可能导致一些问题!
不常见对齐:3字节、5字节等非标准的对齐方式虽然可以使用,但并不常见,且可能在某些平台或编译器上不受支持或导致未定义行为。因此,使用这些非常规对齐方式时需要特别小心。
性能影响:非常规对齐方式可能会对性能产生负面影响,因为现代处理器通常优化了对于2、4、8等字节对齐的数据访问。
兼容性问题:如果你是在做跨平台开发,使用非常规对齐方式可能会导致代码在不同平台上的行为一致性问题。
四、匿名对象
匿名对象指的是在创建对象时,没有显式地给对象命名,而是直接使用该对象进行操作或者传递给其他函数。匿名对象通常在需要临时对象的场景下使用,其生命周期通常限定在当前语句或表达式结束时。
#include<iostream>
using namespace std;
class A
{
public:
void get_int(int val)
{
cout << "get int : " << val << endl;
}
};
int main()
{
A a;
a.get_int(5);
A().get_int(5);//使用匿名对象简化代码,匿名对象的生命周期只有这一行
return 0;
}
运行结果截图:
第二章:成员函数
一、this指针
C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性:
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值(不能手动修改this指针)。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。 - this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递
问题:
- this指针存在哪里?
答:存在栈帧中,vs编译器存在ecx寄存器里。
- this指针可以为空吗?
答:在C++的正常使用中,this指针不应该为空,因为它总是指向调用成员函数的对象。然而,由于程序错误(如访问已释放的对象或内存损坏),可能会导致this指针变得无效。在编写代码时,确保正确管理对象的生命周期和内存,可以避免这些问题。
下面这段代码通过一个赋值为空的类类型指针去调用这个类的成员函数,能成功运行吗?
#include<iostream>
using namespace std;
class a
{
public:
void print()
{
cout << "print()" << endl;
}
void cha(int val)
{
_a = val;
}
private:
int _a;
};
int main()
{
a* ptr = nullptr;
ptr->print();
return 0;
}
代码可以正确运行。a类没有实例化出对象,只是创建了一个a类空指针,用这个空指针去调用成员函数是允许的,因为成员函数并不存放在类对象中。同时,用这个空指针去调用成员函数,因为找不到具体调用的对象,传递的this指针同样为空,但是print()函数的代码没有访问this指针,只是打印一条信息,所以代码可以运行。
运行截图:
那继续去调用cha函数呢?
int main()
{
a* ptr = nullptr;
ptr->cha(5);
return 0;
}
因为cha函数需要访问this指针来访问调用对象的成员变量,但是this指针为空,访问空指针导致程序崩溃。
二、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
例如,我们可以重载运算符“==”,让他支持比较两个date类
#include<iostream>
using namespace std;
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(date d)//重载运算符“==”
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024, 8, 8);
date d2(2024, 8, 9);
d1 == d2 ? cout << "相等" << endl : cout << "不相等" << endl;
return 0;
}
运行结果截图:
我们可以通过这种方式实现原生“==”不支持的功能,运算符重载修改了其在date类中的功能。
注意:
- 不能通过连接其他符号来创建新的操作符。比如operator@
- 重载操作符必须有一个类类型参数(this指针也算)。
- 用于内置类型(int, char等)的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
- “.*” 、“::”、“sizeof”、“?:”、“.”注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
前置++和后置++重载
下面有一个类,包含int和char两个成员变量,我们通过重载++运算符来对成员变量进行修改。
#include<iostream>
using namespace std;
class cl
{
public:
cl(int i, char c)
{
_i = i;
_c = c;
}
void print()
{
cout << _i << _c << endl;
}
cl& operator++()//前置++重载
{
_i += 1;
_c += 1;
return *this;
}
private:
int _i;
char _c;
};
int main()
{
cl a(3, 'c');
++a;
a.print();
return 0;
}
运行结果截图:
因为前置++要返回++之后的结果,也就是返回这个类对象本身,所以我们可以直接返回this指针解引用。
对于后置++,我们需要在参数中增加一个int来标记这是后置++,这里是特殊情况,int只做标记作用。
#include<iostream>
using namespace std;
class cl
{
public:
cl(int i, char c)
{
_i = i;
_c = c;
}
void print()
{
cout << _i << _c << endl;
}
cl& operator++()//前置++重载
{
_i += 1;
_c += 1;
return *this;
}
cl operator++(int)//后置++重载
{
cl tmp(_i, _c);//再创建一个对象来存储++之前的值用来返回。
_i += 1;
_c += 1;
return tmp;//tmp对象作用域仅在当前代码块,不能传引用返回。
}
private:
int _i;
char _c;
};
int main()
{
cl a(3, 'c');
++a;
a++;
a.print();
return 0;
}
运行结果截图:
三、类的六个默认函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数一般分为两种,一种是无参或全缺省的构造函数,称为默认构造函数。另一种是带有参数的构造函数,需要在创建对象时传递参数去调用。
如果用户没有显式实现默认构造函数,编译器会自动生成一个无参的默认构造函数。
注意:默认构造函数自动调用,无需传参。带有非缺省参数的构造函数不是默认构造函数,需要传参!
#include<iostream>
using namespace std;
class date
{
public:
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
return 0;
}
在这个例子中,我们没有显式写date类的默认构造函数,系统会自动生成一个如下的构造函数。
date()
{}
对于这个无参的默认构造函数,会将成员变量中属于内置类型(int, char, duble…)的类初始化成随机值,如果成员变量中含有自定义类型,比如类类型,会去调用这个类的默认构造函数对他进行初始化。
系统生成的默认构造函数将d这个对象中的int类型成员变量初始化为了随机值。
如果我们不想让系统默认初始化为随机值,可以自己编写构造函数。
#include<iostream>
using namespace std;
class date
{
public:
date()
{
_year = 1970;
_month = 1;
_day = 1;
}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
d.print();
return 0;
}
运行结果截图:
如果我们想自己修改日期,可以写带有参数的构造函数。
#include<iostream>
using namespace std;
class date
{
public:
date()//默认构造函数
{
_year = 1970;
_month = 1;
_day = 1;
}
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;//调用默认构造函数
d1.print();
date d2(2024, 8, 8);//调用带参数的构造函数
d2.print();
return 0;
}
运行结果截图:
上面我们提到,默认构造函数一种是无参,一种是全缺省参数,那么我们就可以通过下面的方法将这两个构造函数合二为一。
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
{
_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();
date d2(2024, 8, 8);//调用带参数的构造函数
d2.print();
return 0;
}
运行结果截图:
此时这个构造函数可以作为默认构造函数不传参自动调用,也可以传参自己调用。
特性:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义构造函数,编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
问题:默认构造函数有哪几种?与非默认构造函数有什么区别?为什么一个类至少需要一个默认构造函数?
答:默认构造函数分为下面三种:
1.自己不声明,编译器自己生成的无参的构造函数
2.自己声明的无参的构造函数
3.自己声明的全缺省参数的构造函数
与非默认构造函数的区别:
1. 参数要求:
默认构造函数不需要参数或所有参数都有默认值。
非默认构造函数需要一个或多个明确的参数。
2. 调用方式:
默认构造函数在创建对象时无需提供参数。
非默认构造函数在创建对象时必须提供匹配的参数。
3. 使用场景:
默认构造函数适用于需要默认初始化的情况,例如创建容器中的对象或默认情况下不需要特定初始化的对象。
非默认构造函数适用于需要根据传入参数进行特定初始化的情况。
一个类至少需要一个默认构造函数的原因是在以下情况下会隐式调用默认构造函数:
1. 对象声明但未显式初始化:
当我们声明一个类对象但没有提供初始值时,编译器会隐式调用默认构造函数来初始化对象。如果类没有默认构造函数,这种情况下的对象声明将会导致编译错误。
2. 数组声明:
当我们声明一个类对象的数组时,编译器会尝试隐式调用默认构造函数来初始化数组中的每个元素。如果类没有默认构造函数,这种情况下的数组声明将会导致编译错误。
3. 基类的默认构造函数:
当一个派生类没有显式指定基类构造函数时,编译器会尝试调用基类的默认构造函数。如果基类没有默认构造函数,这种情况下的派生类声明将会导致编译错误。(基类与派生类是继承部分的知识,这里不作详解)
总结:
一般情况下,我们都要自己写构造函数。如果类的成员全部都是自定义类型并且没有想要的特定初始值,可以考虑让编译器自己生成默认构造函数。
初始化列表
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
date(int year, int month, int day)//带参数的构造函数
{
_year = year;
_month = month;
_day = day;
}
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
date(int year = 1970, int month = 1, int day = 1)
//花括号内是构造函数体,可以任意修改成员变量
{
_year = year;
_month = month;
_day = day;
_year += 1;
_day += 2;
}
初始化列表
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
初始化列表才是每个成员定义的地方!!
date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
注意:
-
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
-
类中包含以下成员,必须放在初始化列表位置进行初始化
引用成员变量,const成员变量,自定义类型成员(且该类没有默认构造函数时)。
(这些成员必须在创建时被合适地初始化,否则会引发错误) -
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
下面这段代码运行结果是什么?
#include<iostream>
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 aa(1);
aa.print();
return 0;
}
这段代码似乎用a去初始化_a1,然后用_a1去初始化_a2,那么结果应该是打印出两个1,真的是这样吗?
运行结果截图:
_a1的值为1,但_a2的值为随机值。
因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。_a2比_a1先声明,所以先初始化_a2,_a2用_a1的值去初始化,此时_a1还未初始化,为随机值。初始化_a2之后才用传入的a参数去初始化_a1,_a1被初始化为1。
总结:
构造函数尽量使用初始化列表来初始化成员变量,对于一些初始化列表做不到的,比如再次修改成员变量或一些检查的工作,则在函数体中进行,二者配合使用。
2.析构函数
与构造函数功能相反,析构函数是完成对对象本身的销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
~date()//对于date类编译器默认生成的析构函数
{}
特性:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载 - 对象生命周期结束时,系统自动调用析构函数。
- 编译器生成的默认析构函数,对自定类型成员调用它的析构函数。内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。
与构造函数相同,我们可以自己编写需要的析构函数
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
~date()//析构函数
{
cout << "~date()" << endl;
}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
return 0;
}
运行结果截图:
在这个析构函数中我们打印一条信息来验证系统自动调用了析构函数,对于date类的成员变量我们不需要自己清理,因为编译器会自动清理内置类型成员变量占用的空间。
3.拷贝构造函数
如果我们已经有一个date类对象,想用这个对象的数据去创建一个新的对象,就要用到拷贝构造函数。
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
date(const date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,8,8);
date d2(d1);
d1.print();
d2.print();
return 0;
}
运行结果截图:
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
函数非引用传参实际上传递的是实参拷贝出来的一份临时对象,拷贝这份临时对象也需要调用拷贝构造函数,因为是非引用传参,拷贝这个临时对象又需要拷贝一个临时对象,最终导致一直调用拷贝构造函数,程序崩溃。
- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝,容易出现问题。(资源申请指用new或malloc等去申请空间,这里不作详解)
- 拷贝构造函数典型调用场景:
(1)使用已存在对象创建新对象
(2)函数参数类型为类类型对象
(3)函数返回值类型为类类型对象
4.赋值运算符重载
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
date& operator=(const date& d)//赋值运算符重载
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,8,8);
date d2 = d1;
d1.print();
d2.print();
return 0;
}
运行结果截图:
赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
赋值运算符重载如果不显式实现,编译器会生成一个默认的,对内置类型直接赋值,自定义类型则调用自定义类型的赋值运算符重载。
对于成员变量都为内置类型的date类,我们可以直接用编译器默认生成的赋值运算符重载
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,8,8);
date d2 = d1;
d1.print();
d2.print();
return 0;
}
5.取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容(比如返回一个假地址)
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 1970, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
date* operator&()//取地址操作符重载
{
return this;
}
const date* operator&() const//const取地址操作符重载
{
return this;
}
void print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,8,8);
cout << &d1;
return 0;
}
运行结果截图:
6.const成员
用const修饰的成员函数称之为const成员函数(const加在函数名末尾),const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员变量进行修改。
Date* const this
const成员函数不允许修改this指针指向的成员变量。
问题:
-
const对象可以调用非const成员函数吗?
答:不能,权限放大,const this指针不能传递给this指针 -
非const对象可以调用const成员函数吗?
答:可以,权限缩小,this指针可以传递给const this指针 -
const成员函数内可以调用其它的非const成员函数吗?
答:不能,权限放大,const this指针不能传递给this指针 -
非const成员函数内可以调用其它的const成员函数吗?
答:可以,权限缩小,this指针可以传递给const this指针
7.static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态成员变量一定要在类外进行初始化!
#include<iostream>
using namespace std;
class A
{
public:
void print()
{
cout << _val << endl;
}
private:
static int _val;
};
int A::_val = 1;//在类外初始化,记得加上::标识变量属于哪个类
int main()
{
A a;
A b;
a.print();
b.print();
return 0;
}
运行结果截图:
特性
-
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
-
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
如果在初始化列表定义则会报错
-
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
-
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
-
静态成员也是类的成员,受public、protected、private 访问限定符的限制
第三章:友元与内部类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用(java不提供友元)。
友元分为:友元函数和友元类
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
在外部直接访问A的私有成员变量会报错,我们可以通过友元函数访问。
#include<iostream>
using namespace std;
class A
{
friend void getval(A a);//声明友元函数
public:
A(int val = 0)
:_val(val)
{}
private:
int _val;
};
void getval(A a)//定义友元函数
{
cout << a._val << endl;
}
int main()
{
A a;
getval(a);
return 0;
}
运行结果截图:
我们通过外部的友元函数getval访问到了A类的私有成员变量。
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰(没有this指针)。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制,一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
#include<iostream>
using namespace std;
class Time
{
//声明日期类为时间类的友元类,则在日期类中就能直接访问Time类中的私有成员变量
friend class Date;
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 print()
{
cout << _year << ':' << _month << ':' << _day << ':' << _t._hour << ':' << _t._minute << ':' << _t._second << endl;
}
void SteTimeOfDate(int hour, int minute, int second)
{
//直接访问时间类的私有成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d(2024, 8, 8);
d.SteTimeOfDate(12, 0, 0);
d.print();
return 0;
}
运行结果截图:
注意:
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。 - 友元关系不能传递,只能是单向的
如果C是B的友元, B是A的友元,则不能说明C是A的友元。 - 友元关系不能继承,这里不作详解。
内部类
如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
#include<iostream>
using namespace std;
class A
{
public:
class B//B天生就是A的友元
{
public:
void print(const A& a)
{
cout << _k << endl;
cout << a._h << endl;
}
};
A(int h = 0)
:_h(h)
{}
private:
static int _k;
int _h;
};
int A::_k = 1;
int main()
{
A a;
A::B b;
b.print(a);
return 0;
}
运行结果截图:
注意:
- 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
- 内部类定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
(与单纯的友元类不同) - sizeof(外部类)=外部类,和内部类没有任何关系。
内部类在实际开发中不常用,了解即可。
感谢您的阅读,如有错误期待您的指正!原创不易,如果这篇文章对您有帮助请您点赞收藏支持创作者🥹!后续会分享更多C++知识点干货,期待您的关注!