C++类和对象

目录

类的引入

类的访问限定符及封装 

访问限定符

封装

类的作用域

类的实例化

this指针

成员变量和成员函数的地址 

默认成员函数

构造函数

析构函数

拷贝构造函数

运算符重载

==运算符重载

=赋值运算符重载

初始化列表

explicit关键字 

友元

友元函数

友元类


类的引入

我们在C语言的学习使用过程中,在处理一些任务时总是喜欢使用结构体来进行使用,下面使用创建一个(链)栈类来进行举例:


typedef struct Stack
{
	struct Stack* next; //指向下一空间指针
	int data; //数据存放区域
	struct Stack* top; //指向栈顶指针
    int size; //空间
	int capicity; //内存
}S;

void StackInit() //初始化
{
	//...
}

void StackPush() //压栈
{
	//...
}

void StackPop() //出栈
{
	//...
}

int main()
{
	S s1;
	//...
	return 0;
}

可以看到,我们在C中创建一个栈时,假如需要方便使用,就需要进行typedef重命名操作

而该操作有时候会显得过于冗杂

并且在创建初始化,压栈与出栈函数时,最好需要在前面加上Stack的前缀便于查看调用

否则当一个程序内存在其他的相同类似的函数时就会难以辨认

于是,C++中对其进行了优化及更改:

#include <iostream>

using namespace std;

struct Stack
{
	Stack* next; //指向下一空间指针
	int data; //数据存放区域
	Stack* top; //指向栈顶指针
    int size; //空间
	int capicity; //内存
	
    //在结构体内定义函数
	void Init() //初始化
	{
		//...
	}

	void Push() //压栈
	{
		//...
	}

	void Pop() //出栈
	{
		//...
	}
};

int main()
{
	Stack s1;	
	s1.Init();
	s1.Push();
	s1.Pop();
	return 0;
}

相比于C语言,我们可以看到C++中有以下几个新的变化:

struct升级成为了类

1.结构体名(类)就是类型,可以直接使用类名作为变量的类型

   不需要添加struct或者typedef;

2.类里面可以定义函数;

Ps:需注意,成员函数如果在类中定义,编译器可能会将其当成内联函数处理

而对于上面的代码,在C++中一般使用Class来进行定义

class <className>
{
    // 类体:由成员函数和成员变量组成
};  // 一定要注意后面的分号

再使用上面的栈来举例子:

class stack
{
	//成员变量
	stack* next;
	int data;
	stack* top;
    int size;
	int capicity;

	//成员函数
	void Init()	//初始化
	{
		//...
	}

	void Push() //压栈
	{
		//...
	}

	void Pop() //出栈
	{
		//...
	}
};

这里说一个额外的知识点:成员函数的声明定义分离

我们在做项目时,为了方便用户使用以及读者看懂代码

一般会将函数的声明与定义进行分离(即放在不同的文件内)

在C++中,我们一般需要注意下面这几个点:

//声明函数文件
class stack
{
	//成员变量
private:
	stack* next;
	int data;
	stack* top;
    int size;
	int capicity;

	//成员函数
public:
	void Init(){};

	void Push(){};

	void Pop(){};
};
//定义成员函数文件
void stack::Init() //初始化
{
    stack* newstacktop = (stack*)malloc(sizeof(stack) * 4)
    if(newstacktop = nullptr)
    {
        perror("开辟失败");
        return;
    }
    capicity = 4;
    size = 0;
    top = newstacktop;
    next = null;
}

void stack::Push() //压栈
{
    //...
}

void stack::Pop() //出栈
{
    //...
}

对于此处定义成员函数而言,需要加上stack::域限定修饰符,使编译器在stack类内部寻找目标参数

若在类内部找不到,则会到外部进行查找


但是我们在主函数内调用使用成员函数时,我们会发现有这样的报错:

为什么呢?接下来要引出一个新的知识点


类的访问限定符及封装 

访问限定符

在C++中,访问限定符有三个 public protect private

1. public修饰的成员在类外可以直接被访问

2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

4. 如果后面没有访问限定符,作用域就到 } 即类结束

5. class的默认访问权限为privatestruct为public(因为struct要兼容C)

而对于上面的代码,如果我们想要在主函数内使用成员函数

我们就需要在成员函数前加上限定符修饰:

class stack
{
	//成员变量
private:
	stack* next;
	int data;
	stack* top;
	int capicity;

	//成员函数
public:
	void Init()	//初始化
	{
		//...
	}

	void Push() //压栈
	{
		//...
	}

	void Pop() //出栈
	{
		//...
	}
};

int main()
{
	stack s1;
	s1.Init();
	s1.Push();
	s1.Pop();
	return 0;
}

此处将成员变量定义为private私有类型,将成员函数定义为public公共类型

这样在外部使用这个类时就可以避免直接对成员变量直接修改,只能通过成员函数进行更改

封装

面向对象的三大特性:封装、继承、多态

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中

在类体外定义成员时

需要使用 :: 作用域操作符指明成员属于哪个类域

class Date
{
	//成员变量
private:
	int Year;
	int Month;
	int Day;
public:
	void Init(int Year, int Month, int Day); //初始化
	void Show(); //显示
};

//此处在类外定义类成员函数
void Date::Init(int Year, int Month, int Day)
{
	this->Year = Year;
	this->Month = Month;
	this->Day = Day;
}

void Date::Show()
{
	cout << Year << "_" << Month << "_" << Day;
}

或者简单理解,只要是{ }选区的空间都叫作用域

类的实例化

用类类型创建对象的过程,称为类的实例化

1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它

2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量

class Date
{
	//成员变量
private:
	int _Year;
	int _Month;
	int _Day;
public:
	void Init(int Year, int Month, int Day); //初始化
	void Show(); //显示
};

//此处在类外定义类成员函数
void Date::Init(int Year, int Month, int Day)
{
	_Year = Year; //令类中的_Year等于传入的Year参数
	_Month = Month; //令类中的_Month等于传入的Month参数
	_Day = Day; //令类中的_Day等于传入的Day参数
}

void Date::Show()
{
	cout << _Year << "_" << _Month << "_" << _Day; //输出初始化后的Year,Month和Day成员变量
}

int main()
{
	Date d;
	d.Init(2023, 10, 29);
	d.Show();
}

此处定义一个Date类,并且在主函数内对其进行实例化操作

创建了一个叫做d的Date类型对象,并且调用d的成员函数Init( )和Show( )

这就叫做类的实例化操作

实例化的本质就是 在内存中开辟空间存放对象

this指针

通过上面的Date类的创建,我们为了传入Year,Month和Day进行初始化操作时

为了区分参数与成员变量,我们会在成员变量前加上"_"来做区分

但是这样多少会有些许的不便

我们在C++中可以使用this指针来方便我们的使用

还是以上面的Date类举例:

class Date
{
	//成员变量
private:
	int Year;
	int Month;
	int Day;
public:
	void Init(int Year, int Month, int Day); //初始化
	void Show(); //显示
};

//此处在类外定义类成员函数
void Date::Init(int Year, int Month, int Day)
{
	this->Year = Year; //令成员变量Year等于传入的参数Year
	this->Month = Month; //令成员变量Month等于传入的参数Month
	this->Day = Day; //令成员变量Day等于传入的参数Day
}

void Date::Show()
{
	cout << Year << "_" << Month << "_" << Day;
}

int main()
{
	Date d;
	d.Init(2023, 10, 29);
	d.Show();
}

可以看到,我们在初始化函数Init( )时使用this指针来进行初始化操作

这样便简化了成员函数的命名操作

这就是C++中新引进的关键字this

C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数

让该指针指向当前对象(函数运行时调用该函数的对象)

在函数体中所有“成员变量” 的操作,都是通过该指针去访问

只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

 this指针的特性:

1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值

2. 只能在“成员函数”的内部使用

3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参所,中不存储this指针

4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

所以对于上面的Date类中的Show成员函数,实际上可以看作是这样的原理:

只不过this指针可以进行隐藏

this指针不存放在对象内

一般通过压栈的方式,以寄存器的形式存放在函数栈帧内部(对VS而言,不同编译器不同)

成员变量和成员函数的地址 

我们创建一个类时,会在其中创建一些成员变量和成员函数

但是你知道他们的存放方式吗?下面举一个例子来说:

class Animal {
public:
    void speak() {
        std::cout << "Hello, I am " << name << " and I am " << age << " years old." << std::endl;
    }

    std::string name;
    int age;
};

这里创建了一个Animals类,创建了name和age两个成员变量和一个speak成员函数

下面使用主函数进行对象创建与调用

int main()
{
    Animal cat;
    Animal dog;
    Animal bird;

    cat.name = "Fluffy";
    cat.age = 3;

    dog.name = "Buddy";
    dog.age = 5;

    bird.name = "Tweety";
    bird.age = 1;

    cat.speak();    // Hello, I am Fluffy and I am 3 years old.
    dog.speak();    // Hello, I am Buddy and I am 5 years old.
    bird.speak();   // Hello, I am Tweety and I am 1 years old.
}

可以看到:

尽管我们没有在对象中存储成员函数的地址

但所有的Animal对象都可以共享相同的成员函数代码

因为成员函数并没有绑定到特定的对象上,而是与Animal类本身相关联的

因此,每个Animal对象的名称和年龄都可以通过访问其相应的成员变量来传递给speak()函数

而成员变量就需要先进行初始化操作,随后才能进行访问,否则将会是随机值

这就印证了 成员变量是存放在类中,而成员函数不存放在类中

将成员函数的地址存储在类的静态区域中,可以实现代码的共享和重用,这样有以下好处

  1. 节省内存空间:将成员函数的地址存储在每个对象中将会占用额外的内存空间,如果我们有大量的对象,这样的开销就会非常大。而将成员函数的地址存储在类的静态区域中,只需要存储一份代码,不需要为每个对象都存储一份,因此节省了内存空间

  2. 提高代码的可维护性:如果我们将成员函数的地址存储在每个对象中,那么如果我们需要修改代码,就需要对每个对象都进行修改,这会非常麻烦。而将成员函数的地址存储在类的静态区域中,只需要修改一份代码就可以实现所有对象对该函数的调用

  3. 改善代码的执行效率:因为成员函数的地址是固定的,不需要在每次调用时动态计算,所以将成员函数的地址与对象绑定并没有必要。而通过将成员函数的地址存储在类的静态区域中,可以提高函数调用的效率。并且,所有属于同一类的对象都可以共享相同的成员函数代码,这也能够提高执行效率

因此,将成员函数的地址存储在类的静态区域中,可以提高代码的可维护性、节省内存空间,同时也可以改善代码的执行效率

下面有一个代码,也可以很好的说明这个知识点:

class A
{
public:
    void Print()
    {
        cout << "此处调用了Print函数" << endl;
    }
private:
    int a;
};

int main()
{
    A* p = nullptr; //此处创建了一个指向A类型的p对象
    p->Print();
}

运行结果如下:

可以发现,我们将p置为空指针之后还是可以进行Print函数的调用

通过调试中的反汇编来看一看:

此处就可以很清晰的看到

在执行p->Print( )函数语句时

汇编代码是只将寄存器rcx指向到指针p指向的地址

随后便调用了Print函数,根本就没有在意p是否是空指针这一情况

原因是C++中调用函数是在编译过程中通过函数的地址去进行调用

而对于此处的p->指针没有任何关系

p->指针存在的意义只是因为Print属于成员函数

在传参时会在类中进行成员变量的参数传递,方便使用而已


默认成员函数

首先先来理解,什么叫做默认成员函数?

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

下面来分别介绍一下默认成员函数

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象

他的特征如下:

1. 函数名与类名相同

2. 无返回值

3. 对象实例化时编译器自动调用对应的构造函数

4. 构造函数可以重载                

下面用代码来举例:

class Date
{
private:
    int year;
    int month;
    int day;

public:
    Date() //无参构造函数
    {
        this->year = 1;
        this->month = 1;
        this->day = 1;
    }

    Date(int year, int month, int day) //有参构造函数
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }

    void Init(int year, int month, int day) //自定义初始化函数
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }
    void Print()
    {
        cout << this->year << "_" << this->month << "_" << this->day << endl;
    }
};

int main()
{
    Date d1;
    d1.Init(2023, 10, 30);
    d1.Print();
    Date d2;
    d2.Print();
    Date d3(2023, 10, 15);
    d3.Print();
}

运行结果如下:

可以看到,我们在Date日期类中定义了三个函数,无参构造函数,有参构造函数和初始化函数

并在下方主函数中创建了三个对象d1,d2,d3

d1调用初始化函数并进行传参,输出值为2023_10_30

d2创建对象后没有调用任何函数,直接进行输出操作,输出值为1_1_1

d3创建对象时进行参数传入操作,不调用任何函数,直接进行输出操作,输出值为2023_10_15

而这里的原因正好对应了构造函数的特点:

在d2,d3对象创建时,自动调用执行了构造函数

而上面的有参构造方法正是无参构造方法的重载

所以可以选择传入参数进行初始化,也可以不传入参数进行自定义的默认初始化

Ps:构造函数也支持缺省、

那么问题来了

不是说构造函数时默认成员函数吗?

难道自己写出来的才叫默认吗

在此之前,首先先了解什么叫做默认构造函数

无参构造函数是默认构造函数

全缺省的构造函数是默认构造函数

不主动写构造函数,由编译器自动生成的是默认构造函数

总的来说:

不传参调用的构造函数都叫默认构造函数

(有且只能存在一个默认构造函数)

我们用代码来看:

class Date
{
private:
    int year;
    int month;
    int day;

public:
    void Print()
    {
        cout << this->year << "_" << this->month << "_" << this->day << endl;
    }
};

int main()
{
    Date d1;
    d1.Print();
}

我们在此处再定义一个Date日期类,但是不写构造函数 ,他的运行结果如下:

class Date
{
private:
    int year;
    int month;
    int day;

public:
    Date()
    {

    }

    void Print()
    {
        cout << this->year << "_" << this->month << "_" << this->day << endl;
    }
};

int main()
{
    Date d1;
    d1.Print();
}

 我们在原来的基础上添加上一个无参,无函数体的构造函数,运行结果如下:

class Date
{
private:
    int year;
    int month;
    int day;

public:
    Date()
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }

    void Print()
    {
        cout << this->year << "_" << this->month << "_" << this->day << endl;
    }
};

int main()
{
    Date d1;
    d1.Print();
}

 将构造函数换做带函数体,但是不对他进行传参操作,运行结果如下:

通过上面可以看到

三个代码的运行结果都是输出了三个随机值,这边代表了当不传参操作时

成员变量是无法被初始化的,生成的是随机值

也就是说

即使存在默认构造函数,虽然在实例化对象时会进行调用,但是他也没有任何作用


析构函数

析构函数是什么?

析构函数:

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

它的特征如下:

1. 析构函数名是在类名前加上字符 ~

2. 无参数无返回值类型

3. 一个类只能有一个析构函数

若未显式定义,系统会自动生成默认的析构函数

Ps:析构函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

 下面看一个代码:

class Date
{
private:
	int year;
	int month;
	int day;
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		this->year = year;
		this->month = month;
		this->day = day;
		cout << "此处调用了构造函数~" << endl;
	}
	~Date()
	{
		cout << "此处调用了析构函数~" << endl;
	}
};

int main()
{
	Date d1;
	return 0;
}

我们在这个Date类中定义了构造函数与析构函数

为了可以显示出调用了该函数,我们在其中添加了输出语句

运行结果如下:

可以发现,这个程序先输出了“此处调用了构造函数”,再输出“此处调用了析构函数”

我们知道,构造函数是在实例化对象时由编译器隐性调用

那么析构函数便是在生命周期结束后进行调用

但是构造函数好像在Date类中没有什么用啊

我们换个例子来讲:

class Stack
{
public:
	Stack(size_t capacity = 3) //构造函数进行初始化操作
	{
		cout << "Stack(size_t capacity = 3)" << endl;
		
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
		}

		_capacity = capacity;
		_top = 0;
	}

	~Stack()
	{
		free(_a);
		_capacity = 0;
		_a = nullptr;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack s1;
	return 0;
}

此处我们定义一个Stack栈类,并在主函数中创建对象

我们知道,像这种主动进行malloc开辟空间的代码,一般在程序结束后都需要destory操作

进行空间释放,否则容易造成内存泄漏

但是有时候我们有可能会忘记调用Destory函数进行释放空间操作

这种情况下将释放空间操作放在析构函数中便可实现出作用域自动调用


拷贝构造函数

什么是拷贝构造函数?

拷贝构造函数是一种特殊的构造函数

它是一个使用同一类中已经存在的对象来初始化一个新对象的构造函数

它通常用于执行深拷贝或复制对象的操作,以避免指针指向原对象的问题

如果没有显式地定义拷贝构造函数

编译器会生成一个默认的拷贝构造函数

它将执行浅拷贝(即将指针赋值给新对象),可能导致意想不到的行为

下面用一个Date的代码来举例:

class Date
{
public:
	Date(int year, int month, int day)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
	Date(Date& d) //拷贝构造函数
	{
		this->year = d.year;
		this->month = d.month;
		this->day = d.day;
	}
	void Print()
	{
		cout << year << "_" << month << "_" << day << endl;
	}

private:
	int year;
	int month;
	int day;
};

int main()
{
	Date d1(2023, 11, 1);
	cout << "此处是d1的日期:";
	d1.Print();

	Date d2(d1);
	cout << "此处是d2的日期:";
	d2.Print();
}

我们在此处定义了一个Date类,并实现了构造函数和拷贝构造函数

在主函数中,我们创建了Date对象d1,初始化为2023,11,1

随后创建Date对象d2,传值为对象d1

输出结果如下:

为什么会这样呢?

在C++中,当进行类作为参数进行传参时

编译器会自动调用拷贝构造函数(存在的前提下,若拷贝构造函数没有具体描述则会调用默认拷贝构造函数)

根据拷贝构造函数的函数体进行参数传递

对于这个代码而言

那么问题来了:为什么此处是传递对象d1的别名,而不是传递d1本身呢?

As:看下面的图

此处我们使用传递对象本身进行传参操作

我们知道,传值传参本质上是数值的拷贝,而拷贝就会调用拷贝构造函数

言简意赅的总结:自定义类对象传值需要进行拷贝操作,拷贝操作自动调用拷贝构造函数 

所以我们进行引用对象传值操作

这样就可以避免传参所引发的拷贝数值操作

我们也可以在传值是将参数定义为const Date&,这样就可以避免对引用对象进行修改

运算符重载

运算符重载是 C++ 的一项重要特性,允许程序员为已有的运算符赋予新的行为

通过运算符重载,可以创建自定义的类型,使其支持与内置类型相同的语法和操作

C++中可以重载的运算符包括算术运算符、关系运算符、逻辑运算符、赋值运算符、下标运算符等

他的格式是:

返回类型 operator 运算符 (参数列表) 
{
    // 重载的运算符实现
}

==运算符重载

下面来看一个例子:

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //private:
    int _year;
    int _month;
    int _day;
};

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类d1,d2
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl; //通过==运算符重载进行判断日期是否相同
}

int main()
{
    Test();
}

上面通过重载==运算符,用来比较两个日期是否相同,运行结果如下

但是这也存在着一个问题:

由于运算符重载是作用域是全局

当传参参数为Date类,并且要进行访问时

成员变量就需要变为公有的 

否则会出现以下情况

我们也可以将运算符重载放在内中,作为类成员函数来使用

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }


    bool operator==(const Date& d2)
    {
        return _year == d2._year
        && _month == d2._month
            && _day == d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2023, 11, 20);
    Date d2(2023, 11, 20);
    cout << (d1 == d2) << endl;
}

但是对于类成员函数的运算符重载,我们可以看到,参数少了一个

原因是由于是在类中,可以直接访问类成员

而此运算符重载的参数形式实际上是这样的

bool operator==(Date* this, const Date& d2)

=赋值运算符重载

在 C++ 中,赋值运算符 = 也可以被重载

默认情况下,赋值运算符将一个对象的值赋给另一个对象,但对于自定义类型,在这种默认情况下可能会发生不适当的行为

因此,开发人员可以重新定义赋值运算符以实现自定义的赋值操作

但实际上这听着有点像拷贝构造函数

所以下面我们把这两个结合起来观察一下:

class Date
{
public:
    Date(int year = 1900, 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;
    }

    Date& operator=(const Date& d) //赋值运算符重载
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

    void Print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2023, 11, 20);
    Date d2(d1);
    Date d3 = d1;
    d1.Print();
    d2.Print();
    d3.Print();
}

此处我们先创建了一个Date类d1

随后使用拷贝构造函数创建了一个Date类d2,并使它等于d1

最后我们使用运算符重载创建了Date类d3,Date d3 = d1

运行结果如下:

可以看到,实际上赋值运算符重载与拷贝构造函数所带来的效果是一样的

但细心的朋友会发现,我们在赋值运算符重载时的返回值是Date&引用类型

在这里我们不使用引用也可以实现赋值效果啊

使用引用作为返回值又是为什么呢?

返回引用可以减少对象的拷贝,提高程序的效率

这是因为在 C++ 中,对象的复制是需要消耗时间和空间的操作,尤其是在对象较大或者拷贝频繁的情况下,对程序的性能影响非常大

而返回引用可以避免这种不必要的拷贝,因为它直接将对象的地址传递给调用方,而不是复制一份对象出来

这样可以节省时间和空间,并且减少不必要的性能损失

这与拷贝构造函数有着异曲同工之妙

初始化列表

初始化列表是在创建对象时,用于初始化类成员变量的特殊语法

它位于构造函数的函数体之前,并使用冒号进行标识

class MyClass {
public:
    MyClass(int a, int b) : var1(a), var2(b) {
        // 构造函数体中的其他操作
    }

private:
    int var1;
    int var2;
};

上面这个代码var1(a), var(b)

构造函数的初始化列表 : var1(a), var2(b) 首先指定了成员变量 var1 和 var2 的初始化值

在构造函数的初始化列表中,可以为类的各个成员变量提供初值

这样可以避免在构造函数体中对成员变量进行逐个赋值的操作

而是直接在初始化列表中完成初始化 

使用初始化列表的优点包括:

  1. 效率上的优化:初始化列表将一次性初始化所有成员变量,而不是在函数体中逐个赋值,可以提高执行效率。
  2. 常量成员变量的初始化:常量成员变量只能在初始化列表中进行初始化,不能在构造函数体中赋值。
  3. 对象成员变量的构造:如果类中有其他类的对象作为成员变量,并且需要调用它们的构造函数进行初始化,可以在初始化列表中初始化这些对象。

explicit关键字 

explicit 关键字,用于修饰单参数的构造函数(或转换函数)

限制它们在编译器中的隐式转换行为

这样说有点抽象,我们下面使用一个例子来理解

class Person {
public:
    explicit Person(int age): m_age(age) {} // 显式构造函数

    void printAge() {
        cout << "The age is: " << m_age << endl;
    }

private:
    int m_age;
};

void doSomething(Person p) // 参数对象是通过隐式类型转换传递的
{
    p.printAge();
}

int main()
{
    Person p1(20); // 直接创建对象
    p1.printAge();

    doSomething(p1); // 调用函数,参数对象进行了隐式类型转换
    // doSomething(20); // 会编译错误,因为构造函数是 explicit 的

    return 0;
}

我们定义了一个 Person 类,并为它的构造函数添加了 explicit 关键字

这样,我们在创建对象时必须使用显式的方式传递参数

例如 Person p1(20) ,而不能使用隐式类型转换,例如 Person p2 = 20 

同时,我们在 doSomething 函数中调用了 Person 对象的方法

注意,这里函数参数是 Person 对象类型,并且实际传递是使用隐式类型转换方式的

如果构造函数没有使用 explicit 关键字,则传递时会自动进行隐式类型转换,无需使用显式方式传递参数

需要注意,在使用 explicit 关键字时,我们只能设定单参数的构造函数为 explicit ,而不能设定多参数的构造函数为 explicit

那么问题来了

假如我这里不使用 explicit 关键字的话,可以使用doSomething(20)进行调用传值吗?

class Person {
public:
    Person(int age) : m_age(age) {} // 隐式构造函数

    void printAge() {
        cout << "The age is: " << m_age << endl;
    }

private:
    int m_age;
};

void doSomething(Person p) // 参数对象是通过隐式类型转换传递的
{
    p.printAge();
}

int main()
{
    Person p1(20); // 直接创建对象
    p1.printAge();

    doSomething(p1); // 调用函数,参数对象为Person类型,不进行转换
    doSomething(20); // 调用函数,参数对象为Int类型,进行转换

    return 0;
}

运行结果如下:

如果将 Person 构造函数改为不带 explicit 关键字的隐式构造函数

则编译器可以自动将整数20隐式类型转换为 Person 对象

使其作为参数传递给 doSomething() 函数,从而成功编译

在这种情况下,编译器首先查找接收单个参数的 doSomething() 函数的声明

发现它需要一个 Person 类型的参数

然后,编译器查找符合隐式转换规则的构造函数

发现 Person 类型的构造函数需要一个整数参数

因此编译器自动将整数 20 转换为 Person 对象,然后将其传递给 doSomething() 函数

友元

友元函数

友元函数是在一个类中声明的,但不是该类的成员函数的函数

它们被声明为类的友元,并被授权访问该类的私有成员和保护成员

使用friend关键字

下面一个例子更好的理解:

class Square {
private:
    double side;

public:
    Square(double s) : side(s) {}

    // 声明友元函数
    friend double calculateArea(const Square& s);
};

// 定义友元函数,可以直接访问 Square 类的私有成员 side
double calculateArea(const Square& s) {
    return s.side * s.side;
}

int main() {
    Square square(5.0);
    double area = calculateArea(square);
    std::cout << "Area of square: " << area << std::endl;

    return 0;
}

在这个例子中,我们有一个名为 Square 的类

其中包含私有成员 side 表示正方形的边长

类中声明了一个友元函数 calculateArea 用于计算正方形的面积

在 main 函数中,我们创建了一个 Square 对象,并调用了友元函数 calculateArea 来计算面积

由于 calculateArea 是 Square 类的友元函数,它可以直接访问 Square 类的私有成员 side 

友元函数有以下特点:

  1. 可以直接访问类的私有成员和保护成员:友元函数被授予了与类的成员函数相同的访问权限,可以直接访问类的私有成员和保护成员,即使这些成员在类的外部是不可访问的

  2. 不属于类的成员函数:友元函数可以在类的外部定义和实现,它们不是该类的成员函数。因此,它们不会继承类的特性,也不受类的访问控制机制的限制

  3. 声明方式:友元函数的声明通常在类的内部,在类的声明中使用 friend 关键字声明。然后,在类的外部,可以定义并实现该友元函数

友元函数可以用于以下情况:

  1. 需要访问类的私有成员和保护成员,但又不想将这些成员设置为公有的或提供额外的接口函数

  2. 需要在类的外部实现某些与类紧密相关的函数,但这些函数在逻辑上不属于类的成员函数

友元类

友元类是指在一个类中声明的另一个类

该类被授权访问声明它为友元的类的私有成员和保护成员

友元类可以访问被授权类的所有成员,包括私有成员、保护成员和公有成员

下面一个例子更好的理解:

class Square {
private:
    double side;

public:
    Square(double s) : side(s) {}

    friend class Rectangle; // 声明 Rectangle 类为友元类
};

class Rectangle {
private:
    double length;
    double width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}

    void printArea(const Square& s) {
        double area = s.side * length * width;
        std::cout << "Area: " << area << std::endl;
    }
};

int main() {
    Square square(5.0);
    Rectangle rectangle(4.0, 6.0);
    rectangle.printArea(square);

    return 0;
}

在这个例子中,我们有两个类: Square 和 Rectangle 

在 Square 类中,我们将 Rectangle 类声明为友元类

以便让 Rectangle 类能够访问 Square 类的私有成员 side 

在 Rectangle 类中,我们定义了一个函数 printArea 

该函数接收一个 Square 对象,并计算正方形面积和矩形的面积乘积

在 main 函数中,我们创建了一个 Square 对象和一个 Rectangle 对象

并调用 Rectangle 类的 printArea 函数,以便使用 Square 对象的 side 成员计算面积乘积

友元类有以下特点:

  1. 对授权类的访问权限:友元类可以访问被授权类的所有成员,与该类的成员函数类似,友元类也被授予了与被授权类相同的访问权限

  2. 声明方式:在被授权类的声明中使用 frined 关键字声明友元类。被授权类声明为友元类后,友元类可以直接访问被授权类的私有成员和保护成员

友元类的使用场景包括:

  1. 类之间的紧密关系:当两个类之间具有密切的关系,需要彼此访问私有成员和保护成员时,可以使用友元类

  2. 封装性的一种例外:友元类打破了类的封装性,使得在某些情况下更方便地共享类的成员

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值