一.前言
提及到C++我们总会抛出一句话:C语言是面向过程的,C++是基于面向对象的。那么到底什么是面向过程什么是面向对象呢?二者又有什么区别呢?
面向过程(Procedural Programming): 面向过程编程更侧重于步骤和函数的组合来解决问题。程序被设计为一系列有序的步骤,每个步骤对应一个函数或子程序,这些函数直接操作数据。面向过程编程不强调对象的概念,而是以数据为中心,通过函数来处理数据。
面向对象(Object-Oriented Programming, OOP)是一种编程范式,它基于“对象”的概念,将数据和操作数据的方法组织在一起。在面向对象编程中,对象是类的实例,类定义了对象的属性(数据成员)和行为(方法)。对象可以互相通信,通过调用彼此的方法来完成任务。面向对象的四个核心原则是封装、继承、多态和抽象。
面向对象的三大特性:
- 封装:隐藏对象的内部细节,只对外提供接口进行交互,保护数据的安全性。
- 继承:允许创建一个新类(子类)作为现有类(父类)的扩展,继承其属性和方法。
- 多态:同一方法可以根据调用它的对象类型表现出不同的行为。
面向对象与面向过程的区别:
- 编程思路:面向对象是基于类和对象,通过对象之间的交互实现功能;面向过程是通过函数调用来完成任务序列。
- 封装性:面向对象封装的是数据和操作数据的方法,而面向过程主要封装的是功能逻辑。
- 结构与复用:面向对象支持继承和多态,使得代码更容易复用和扩展;面向过程的复用主要依赖函数和模块。
- 复杂性管理:面向对象更适合处理复杂的系统,因为它能更好地模拟现实世界中的实体和关系;面向过程则适用于简单的、线性的任务。
二.类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。上面结构体的定义,在C++中更喜欢用class来代替。
三.类的定义
class className{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理(内联函数后面讲)。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
类声明:
类函数定义:
一般情况下,更期望采用第二种方式;
四.类的封装
访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
问题:C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。在继承和模板参数列表位置,struct和class也有区别。在继承中,使用struct和class关键字指定基类时有不同的含义。如果使用struct继承,则默认是public继承;如果使用class继承,则默认是private继承。至于再模板上面的区别,我们再模板部分讲。
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,让用户更方便使用类。
比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
Q.C++中如何实现类的封装?
在C++语言中实现封装,可以通过类(class)将数据以及操作数据的方法进行有机结合,通过访问权限符来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
五.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。如下:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
类中成员变量可以在任意"成员函数中使用",因此:成员变量可以看成是成员函数的全局变量;
C++中作用域:全局作用域、函数体中局部作用域、命名空间域、类域
六.类的实例化
用类类型创建对象的过程,称为类的实例化。
1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
类和对象的区别:
- 定义上的区别:
- 类(Class):类是一个模板或蓝图,它定义了创建对象时所需的属性和方法。类是对一组具有相同属性和方法的对象的抽象描述。
- 对象(Object):对象是类的实例。它是根据类的定义创建的具体实体,具有类中定义的属性和方法。
- 用途上的区别:
- 类:用于定义和描述一组具有共同属性和行为的对象。类定义了对象的数据结构(属性)和操作这些数据的方法(行为)。
- 对象:用于表示和存储现实世界中的具体实体。对象是程序运行时数据的容器,它根据类的定义来存储数据和执行操作。
- 存在形式上的区别:
- 类:类是一个抽象的概念,它只存在于代码编译阶段,不占用内存空间(除了类的元数据)。
- 对象:对象是类的具体实例,它在程序运行时被创建并占用内存空间。对象的属性和方法根据类的定义来分配和初始化。
- 关系上的区别:
- 类与对象:类是对象的模板,对象是类的实例。一个类可以创建多个对象,这些对象具有相同的属性和方法,但属性值可能不同。
- 对象与对象:对象之间是相互独立的,它们根据类的定义具有相同的结构和行为,但存储的数据可能不同。
计算类对象大小
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
A1 a1;
A2 a2;
A3 a3;
cout << "A1:" <<sizeof(a1)<< " A2:" <<sizeof(a2)<< " A3:" <<sizeof(a3)<< endl;
return 0;
}
所以一个类的大小,实际就是该类中”成员变量”之和,与成员函数无关。因为成员变量(属性)是存储在对象实例的内存空间中的,成员函数存放在公共的代码段。当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
在C++中,即使是空的类,也可以被实例化。每个实例(对象)在内存中都需要一个独一无二的地址,以便程序能够区分它们。如果空类的大小为0,那么实例化多个对象时,它们在内存中的地址可能会重叠,从而无法区分这些实例。
内存对齐
内存对齐是数据在内存中的存储位置需要按照一定的规则进行对齐,以提高内存访问的效率。
为什么需要内存对齐?
提高访问速度:现代计算机的内存访问通常是以一定的块(如字节、字、双字等)为单位进行的。如果数据没有按照这些单位进行对齐,那么访问这些数据时可能需要多次内存访问,从而降低了访问效率。
满足硬件要求:某些硬件平台可能要求特定的数据对齐方式,否则可能会引发硬件异常或产生不正确的结果。
结构体内存对齐
这里我偷个懒,挂个链接(写的很不错),大家自己看看。
补:再VS中,默认对齐数是8,如果我要指定VS默认对齐数呢?
使用#pragma pack
可以用来临时改变结构体或联合体的成员对齐方式
#pragma pack(push, 4) // 保存当前对齐状态,并设置新的对齐数为4
struct MyStruct {
char a;
int b;
short c;
};
#pragma pack(pop) // 恢复之前的对齐状态
大小端
大端字节序(Big-Endian):在这种方式中,数据的最高有效字节(MSB,Most Significant Byte)存储在内存的低地址端,而最低有效字节(LSB,Least Significant Byte)存储在内存的高地址端。换句话说,数据从低地址到高地址的存储顺序与数据的字节表示顺序相同。
小端字节序(Little-Endian):与大端字节序相反,小端字节序将数据的最低有效字节存储在内存的低地址端,而最高有效字节存储在内存的高地址端。这意味着,在内存中,数据从低地址到高地址的存储顺序与数据的字节表示顺序是相反的。
Q.如何测试某台机器是大段还是小段呢?
根据上面的图我们可以简单分析到,我们去前8位的地址,也就是一个字节的地址,也就是一个char,然后判断是不是等于0,如果等于0,则是大端,否则是小端。
int main()
{
int i = 1;
char* t = (char*)&i;
if (*t == 0)
{
cout << "这个是大端" << endl;
}
else
{
cout << "这个是小端" << endl;
}
return 0;
}
七.This指针
class Date
{
public:
void Init(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 a;
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
This指针有可能是nullptr吗? 可能,但一般情况下不为空。
这里为什么可以,且能跑,是因为当程序编译后,成员函数的地址即已经确定。另外,C++只关心你的指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性。nullptr对象调用成员函数时,只要不访问此对象独有的内存部分(成员变量),则程序正常运行,因为不会使用this,一旦访问此对象的成员变量,则程序崩溃。 如下:
This指针存在哪里
this
指针是在C++类中的一个特殊指针,它指向当前对象的地址。在类的成员函数中,可以使用this
指针来访问当前对象的成员变量和成员函数。在C++中,每个非静态成员函数都隐含地包含一个this
指针。即this
是个形参,存放在栈区中,或叫ecx
寄存器,上述图片可以直接展现编译器将d1
的地址存放到寄存器中。
八.类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成 员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
Date d2(111, 11, 1); 将Date类中构造函数放开后,这样才是对的
return 0;
}
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参 构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
7.构造函数不能被
const
修饰8.构造函数不能是虚函数
关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的 构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象 _year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如: int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发 现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数 。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时 可以给默认值。
初始化列表
初始化列表时构造函数的一部分,或者说,加上初始化列表才是完整的构造函数。因为初始化列表是真正的对成员变量进行初始化,而构造函数体中是赋值。初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成 的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
5编译器生成的默认 析构函数,对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
// 程序运行结束后输出:~Time()
// 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是
// 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所
以在
// d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函
数
// 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构
函
// 数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用
Time
// 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有 资源申请时(malloc,new...),一定要写,否则会造成资源泄漏,比如Stack类。
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型 对象创建新对象时由编译器自动调用。
特征:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发 无穷递归调用。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
因为使用传值的方式传参,那么会发生拷贝即形参是实参的拷贝。而在这里,形参和实参都是类类型对象,所以调用拷贝构造函数,因为拷贝构造函数使用传值的方式传参,那么.....。最后陷入死循环。
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;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构 造函数是一定要写的,否则就是浅拷贝.
4.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调 用其拷贝构造函数完成拷贝的
赋值运算符重载
运算符重载
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表
运算符重载和函数重载的区别?
函数重载指的是在同一个作用域下,一组函数的函数名相同,但是形参列表(类型、顺序、个数)不同,这样的函数就构成了函数重载。注意:函数返回值类型和函数重载无关
//构成函数重载
int add(int a, int b)
{
return a + b;
}
int add(int a, int b,int c)
{
return a + b + c;
}
float add(float a, int b)
{
return a + b;
}
float add(int a, float b)
{
return a + b;
}
//下面两个函数不构成函数重载
//返回值类型不是函数重载的决定因素
float add(int a, float b)
{
return a + b;
}
double add(int a, float b)
{
return a + b;
}
c语言不支持函数重载的原因
c语言进行函数修饰时只用到了函数名的信息,函数名相同修饰之后也是一样的名字,对于同名函数编译器没办法区分,会认为函数重定义,所以不支持函数重载。C++函数重载的底层原理
对于同名函数,C++根据函数名和参数列表对函数名进行修饰,修饰之后同名的函数编译器也能进行区分。
例如:g++的函数修饰规则是
_Z+函数名字符个数+函数名+形参列表类型首字母因此,同名函数如果只有返回值类型不相同,修饰之后编译器还是没办法区分,会认为函数重定义。
运算符重载的注意事项:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
4. .* :: sizeof ?: . 注意以上5个运算符不能重载。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//做为类成员函数时
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
//private:
int _year;
int _month;
int _day;
};
// 全局的operator==
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
赋值运算符重载
1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
2.赋值运算符只能重载成类的成员函数不能重载成全局函数 .赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了。
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的 赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类 型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
4.编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝(浅拷贝)了,还需要自己实现吗?当然
如果类中涉及到资源管理时,用户必须要显式提供赋值运算符重载,否则可能会造成内存泄漏或者运行时崩溃,用户一般是按照深拷贝方式提供的 。
细心的朋友会发现,这里只讲到了,构造函数,析构函数,拷贝构造函数,赋值运算符重载。这才四个默认成员函数,还有两个呢?还有两个是普通对象取地址重载和const对象去地址重载,只不过这两个很少自己写,编译器默认生成的完全够用了。
九.const成员
const修饰成员函数。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。除非该变量被nutable修饰。且该成员函数只能调动const成员函数,不能调动非const成员函数。
const修饰的成员变量不能再成员函数中被修改,且必须再类初始化列表中初始化。
补充:
const关键字的作用?
const
可以用来定义常量,即一旦初始化后就不能被修改的值。const
可以用来修饰指针,表明指针所指向的内容是常量(指向常量的指针)或者指针本身是常量(常量指针)或者指向常量的常量指针。onst
可以用来修饰传入参数,表明函数内部不会修改这些参数的值。- const修饰的常量和宏常量的区别?
- (1) 编译器处理方式不同: define宏是在预处理阶段展开。 const常量是编译运行阶段使用。(2) 类型和安全检查不同:define宏没有类型,不做任何类型检查,仅仅是展开。const常量有具体的类型,在编译阶段会执行类型检查。(3) 存储方式不同: define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。) const常量会在内存中分配(可以是堆中也可以是栈中)。
const对象可以调用非const成员函数吗?
答案是不可以。const对象只能调用const成员函数。非const成员函数可能会修改对象的成员变量,而const对象保证在其生命周期内其状态(即成员变量的值)不会改变。因此,从逻辑上讲,不允许const对象调用可能会改变其状态的非const成员函数。
非const对象可以调用const成员函数吗?
答案是可以。非const对象可以调用任何成员函数,包括const成员函数。const成员函数承诺不会修改任何成员变量的值,因此,非const对象调用const成员函数是安全的。
const成员函数内可以调用其它的非const成员函数吗?
答案是不可以,直接调用是不允许的。由于const成员函数承诺不会修改任何成员变量的值,因此它不能调用可能修改成员变量的非const成员函数。如果需要在const成员函数内部调用非const成员函数,你通常需要重新考虑设计,因为这通常违反了const成员函数的目的。不过,一种可能的解决方案是通过非const成员函数或其他机制(如友元函数或静态成员函数)间接实现。
非const成员函数内可以调用其它的const成员函数吗?
答案是可以。非const成员函数可以调用const成员函数,因为const成员函数不会修改任何成员变量的值,因此从非const成员函数内调用它们是安全的。这种调用是常见的,尤其是在非const成员函数需要访问或处理数据,但又不想修改数据时。
十.静态成员函数
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
特性:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
静态成员变量:
1.静态成员变量不能在初始化列表位置初始化,必须在类外进行初
始阿化,在类外初始阿化时必须要加类名::,类中只是声明
2.静态成员变量是类的属性,不属于某个具体的对象,是类所有对
象共享的
3.不存在在具体的对象中,因此不会影响sizeof的结果
4,可以通过对象.静态成员名,也可以通过类名::静态成员变量名方
式访问
5.在程序启动时,就完成了对静态成员变量的初始化工作
静态成员函数:
1.静态成员函数没有this指针
2.静态成员函数中不能直接访问非静态成员变量,因为所有非静态成员变量都是通过this指针访问的
3.静态成员函数中不能调用普通成员函数。因为静态成员函数没有this指针,而普通成员函数有this指针,导致参数个数不匹配。非静态成员函数可以调用类的静态成员函数
4.静态成员函数不能被this修饰
5.静态成员函数不能是虚函数。因为虚函数通过this
指针隐式地访问对象的成员。由于静态成员函数没有this
指针,因此它们无法以虚函数的方式工作。
6.既可以通过对象,也可以通过类名::方式访问
十一.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类
友元函数
我们现在类中现在尝试去重载operator<<后发现:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,8,17);
d1 << cout;//等于这种写法d1.operator<<(cout);
//不符合常规调用:cout<<d1;
return 0;
}
若要使用常规调用:cout<<d1。那么cout需要是第一个形参对象,但是若是operator<<重载为成员函数,第一个参数又必须时This指针.所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。如下:
从上面我们可以看到。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
特性:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制一个函数可以是多个类的友元函数4.友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
特性:
1.友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
3.友元关系不能继承。
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;
}