C++ 类 基础篇

目录

八股文

问:为什么c++要用类?

实验1 展示类的封装

类的实例化

实验2

实验3

​编辑

类的声明和定义分离

类对象的存储方式

求A1,A2,A3的大小

this指针

 测试小题:​编辑

 第二题:

默认成员函数

 构造函数

析构函数

内联函数


八股文

什么是对象

c++通过类表示对象.

问题:C++中struct和class的区别是什么?

解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大 家介绍。

问:为什么c++要用类?

c++用类是为了方便封装。

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日 常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。 对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。 在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

实验1 展示类的封装

首先, 我们封装一个Stack类:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

struct Stack
{
	void  Init()
	{
		a = nullptr;
		top = capacity = 0;
	}

	void push(int x)
	{
		if (top == capacity)
		{
			size_t  newcapacity = capacity == 0 ? 4 : capacity * 2;
			a = (int*)realloc(a, sizeof(int) * newcapacity);
			capacity = newcapacity;
		}
	}
	int *a;
	int top;
	int capacity;				
};

int main()
{
	Stack st2;
	st2.Init();
	st2.push(1);
	st2.push(2);
	st2.push(3); 
	st2.push(4);

	return 0;
}

 假设现在一个用户想访问我们写的这个Stack类里面的栈顶元素,它就这样访问:

cout << st2.a[st2.top] << endl;

结果打印出来是随机值:

 我们作为这个写这个Stack类的人当然知道他犯了什么错误:他访问的不是栈顶元素,而是-1的位置。真正的栈顶在下标为0的位置。

为了防止使用者出错,我们干脆不让使用者直接访问我们的元素,而是将其封装为一个函数。

同时将Stack里面的变量设为隐私,防止使用者直接访问:

	int Top()
	{
		return a[top - 1];
	}


private:
	int *a;
	int top;
	int capacity;	

这样使用者想要访问栈顶元素,就只能通过访问Top()函数来访问了(Top()函数设置了正确的栈顶位置,不会访问错):

cout << st2.Top() << endl;

类的实例化

实验2

假设data类有三个成员变量:year,month,day。我们现在需要传参初始化一下data类,于是写了如下代码:

class date
{
public:
	void Init(int year,int month,int day)
	{
		year = year;
		month = month;
		day = day;
	}
private:
	int year, month, day;
};

int main()
{
	date d1;
	d1.Init(2023,11,29);
	return 0;
}

很明显,容易混淆。为了防止搞混淆,我们把类里面的成员变量前面都加个杠,变为如下所示:

class date
{
public:
	void Init(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year, _month, _day;
};

int main()
{
	date d1;
	d1.Init(2023,11,29);
	return 0;
}

实验3

假如去掉类成员变量的private限定符,可以直接访问类的成员变量吗?

答案:不可以。如下:

原因

 首先我们要知道datet里面的成员变量都只是声明:

    int _year;
	int _month;
	int _day;

所谓声明就是没开辟空间,不能直接访问。

我们需要先定义一个datat类型的变量,对date类进行一个实例化,这样就把空间开辟出来了。我们再通过这个变量区访问date的成员变量:

如果此时我们把date的成员变量用private限定一下,也不能直接访问了,我们需要借助Init()初始化去访问:

类的声明和定义分离

.h文件里面类成员变量不动,类成员函数只留声明:


class Stack
{
public:
	void  Init();

	void push(int x);

	int Top();


private:
	int* a;
	int top;
	int capacity;
};

.cpp文件里写成员函数的定义,注意要用类域限定符才能访问,否则编译器不知道你成员函数里面访问的变量是类的成员变量还是全局变量:


	void Stack::Init()
	{
		a = nullptr;
		top = capacity = 0;
	}

	void Stack::push(int x)
	{
		if (top == capacity)
		{
			size_t  newcapacity = capacity == 0 ? 4 : capacity * 2;
			a = (int*)realloc(a, sizeof(int) * newcapacity);
			capacity = newcapacity;
		}
		a[top++] = x;
	}

	int Stack::Top()
	{
		return a[top - 1];
	}



类对象的存储方式

输出一下下面代码的结果:

答案:

12字节是date类的三个成员变量所占的内存大小。因此我们可以知道类的成员函数并不存储在类的内部。

原因:

如下图所示,d1和d2的_year并不是一个变量,因为是两个对象的_year。

但是d1和d2调用的pirnt()确是一个函数,因此函数需要放在公共代码区,大家都可以用,但是变量的话各自用各自的:

求A1,A2,A3的大小



class A1 {
public:
	void f1() {}
private:
	char _ch;
	int _a;
};

// 类中仅有成员函数
class A2 {
public:
	void f2() {}
};

// 类中什么都没有---空类
class A3
{};
int main()
{
	cout << sizeof(A1) << endl;
	cout << sizeof(A2) << endl;
	cout << sizeof(A3) << endl;
	return 0;
}

答案:

8
1
1

A1:结构体对齐

 A2:成员函数实际上放在公共代码区,所以成员函数不占类的空间。

A3:虽然类为空,但是如果都是0字节,那地址都没有,怎么区分与其他类,所以象征性给个1字节,这个字节叫占位符

this指针

this指针的作用

1.帮我们解决命名冲突问题

如下所示,这段代码输出的age的值是否是18?

#include<iostream>
using namespace std;

class Person
{
public:
	Person(int age)
	{
		this->age = age;
	}
	int age;
};
int main()
{
	Person p1(10);
	cout << "age: " << p1.age << endl;

	return  0;
}

答案是:不是.

这是因为我们可以编译器认定类里面的成员变量age和我们构造函数无关.

所以我们的成员变量age并没有被正确赋值.有两种解决办法,第一种是给成员变量起名和构造函数参数相异,如下:

#include<iostream>
using namespace std;

class Person
{
public:
	Person(int age)
	{
		_age = age;
	}
	int _age;
};
int main()
{
	Person p1(10);
	cout << "age: " << p1._age << endl;

	return  0;
}

第二种办法就是用this指针,如下:

#include<iostream>
using namespace std;

class Person
{
public:
	Person(int age)
	{
		this->age = age;
	}
	int age;
};
int main()
{
	Person p1(10);
	cout << "age: " << p1.age << endl;

	return  0;
}

我们可以看见this执行的age和我们的成员变量age是同一个age.而构造函数参数age和右值age是一个age:

这是因为this指向的是被调用的成员函数所属的对象.我们这里调的是构造函数,调用的对象是p1,所以this指向的就是p1.

2.返回对象本身用*this

如下代码所示实现年龄加10功能:

#include<iostream>
using namespace std;

class Person
{
public:
	Person() {}
	Person(int age)
	{
		_age = age;
	}

	void addage(Person& p)
	{
		_age += p._age;
	}
	int _age;
};
int main()
{
	Person p1(10);
	Person p2(10);
	p2.addage(p1);
	cout << "年龄: " << p2._age << endl;

	return  0;
}

如果我想年龄再加10,那我再调一次addage()函数,就报错了.

我们传过去的时候this指针指向的是p2,所以p2可以调用addage()函数,但是返回的时候因为addage()函数的返回值是void,所以P2不能接着调用addage()了.如果返回p2这个对象,那么p2才可以接着调用addage();

因此我们可以返回p2这个对象,通过*this解引用找到p2,将其返回,这样p2就可以接着调用addage()函数了:

#include<iostream>
using namespace std;

class Person
{
public:
	Person() {}
	Person(int age)
	{
		_age = age;
	}

	Person& addage(Person& p)
	{
		_age += p._age;
		return *this;
	}
	int _age;
};
int main()
{
	Person p1(10);
	Person p2(10);
	p2.addage(p1).addage(p1).addage(p1);
	cout << "年龄: " << p2._age << endl;

	return  0;
}

对于上述类,有这样的一个问题: Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函 数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢? C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。

实际上是怎么运行的呢?

 测试小题:

答案:正常运行

 解析:

 p->print()如果不理解可以用(*p).PrintA()来理解,二者一样:

 第二题:

 答案:运行崩溃

这是因为 

 空指针调用成员函数引发异常

下面这段代码运行之后不会输出年龄,而是会崩掉:

#include<iostream>
using namespace std;

class Person
{
public:
	Person()
   {
		cout << "this is a构造" << endl;
   }
   void showage(int age)
   {
		_age = age;
		cout << "年龄: " << _age << endl;
   }
private:
	int _age;
};
int main()
{
	Person *p1=NULL;
	p1->showage(10);
	return  0;
}

 这是因为_age实际上前面有个this指针,而this指针指向的就是调用成员函数的对象,也就是p1,而p1是空值,因此会引发中断:

const修饰成员函数

常函数

下面代码中可以在showPerson()函数中修改成员变量_age:

#include<iostream>
using namespace std;

class Person
{
public:
	void showPerson()
	{
		_age= 10;
		cout << "年龄: " << _age << endl;
	}
private:
	int _age;
};
int main()
{
	Person p1;
	p1.showPerson();
	return  0;
}

 如果我不想showPerson()函数能修改成员变量,那我们可以给其加上const修饰符,将其变为常函数:

 加了const之后就不能修改成员变量_age的原因是this->_age,this指针又是一个指针常量,所谓指针常量就是指向的对象不能变. 它的类型如下:

Person *const this  //指向的对象不能变

 现在我们不想让其指向的对象被修改,就再加个const即可:

const Person *const this  //指向的对象的值不能变

常对象

给对象加上const修饰符就能将其变为常对象,常对象只能调常函数:

友元函数

全局函数做友元

有如下结构:

#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	Person()
	{
		_sittroom = "客厅";
		_bedroom = "卧室";
	}
public:

	string _sittroom;  //客厅

private:
	string _bedroom;  //卧室
};

void goodfriend(Person *p)
{
	cout << "好朋友正在访问: " << p->_sittroom << endl;
	cout << "好朋友正在访问: " << p->_bedroom << endl;
}
void tes01()
{
	Person p;
	goodfriend(&p);
}
int main()
{
	tes01();

	return  0;
}

goofriend()函数如下:

 我们可以发现好朋友是可以正常访问客厅的,但是不能正常访问卧室.现在我想让他访问卧室,我就将其声明为我的友元函数,就是将在声明在类内部重新声明,且加上friend关键字修饰:

 

类做友元

如下所示,好朋友类是访问不了房间类里的私有成员卧室的:

#include<iostream>
#include<string>
using namespace std;

class Room
{
public:
	Room()
	{
		_sittroom = "客厅";
		_bedroom = "卧室";
	}
public:
	string _sittroom;
private:
	string _bedroom;

};
class goodfriend
{
public:
	goodfriend()
	{
		room = new Room;
	}

	void vist()
	{
		cout << "好朋友正在访问: " << room->_sittroom << endl;
		cout << "好朋友正在访问: " << room->_bedroom << endl;
	}

private:
	Room* room;
};

void test01()
{
	goodfriend f1;
	f1.vist();
}
int main()
{
	test01();
	return 0;
}

但是我们可以将好朋友类声明为友元类:

 

成员函数做友元

#include <iostream>  
#include <string>  

using namespace std;

// Room 类的声明  
class Room;

// goodfriend 类的声明,它使用 Room 类型的指针  
class goodfriend {
public:
    goodfriend();
    void vist();

private:
    Room* room;
};

// Room 类的定义  
class Room {
public:
    Room() : _sittroom("客厅"), _bedroom("卧室") {}

    string _sittroom;
private:
    string _bedroom;
};

// goodfriend 类的构造函数实现  
goodfriend::goodfriend() : room(new Room) {}

// goodfriend 类的 vist 方法实现  
void goodfriend::vist() {
    cout << "好朋友正在访问: " << room->_sittroom << endl;
    cout << "好朋友正在访问: " << room->_bedroom << endl;
}

// 测试函数  
void test01() {
    goodfriend f1;
    f1.vist();
}

int main() {
    test01();
    return 0;
}

把vist() 函数声明为Room类友元函数:

 

默认成员函数

即自己不写,编译器自己会写的函数

 构造函数

 如果不初始化直接使用会产生随机值或者报错:



	DATA S1;

	S1.print();


	DATA S2;

	S2.print();
-858993460/-858993460/-858993460

-858993460/-858993460/-858993460

 而我们写个构造函数就可以完成初始化:


class DATA
{
public:
	//void Init(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
//构造函数
	 DATA()
	 {
		 _year = 1;
		 	_month = 1;
		 	_day = 1;
	 }

	void print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
		printf("\n");
	}
private:
	int _year;
	int _month;
	int _day;

};
int main()
{
	DATA s1;
	s1.print();
1/1/1  <--初始化完成-->

构造函数的用法:

 构造函数也可以带参数,或者全缺省

 DATA(int year,int month,int day)
	 {
		 _year = year;
		 _month = month;
		 _day = day;
	 }
int main()
{
	DATA s1(1,1,1);
	s1.print();
1/1/1

写带参的构造函数在调用时也要传参,否则就会报错:

 我们也可以给参数赋缺省值


	 DATA(int year=1, int month=1, int day=1)
	 {
		
		  _year = year;
		  _month = month;
		  _day = day;
	 
	 }

 但是缺省值的构造函数和不缺省的同一个类构造函数不能同时出现,不构成函数重载

DATA(int year,int month,int day)
	 {
		 _year = year;
		 _month = month;
		 _day = day;
	 }


	 DATA(int year=1, int month=1, int day=1)
	 {
	
		  _year = year;
		  _month = month;
		  _day = day;
	 
	 }

 假如我们要push1000个数进栈,那就会不断扩容,扩到后面空间太大需要异地扩容。

异地扩容0字节
异地扩容4字节
异地扩容8字节
异地扩容16字节
异地扩容32字节
原地扩容64字节
原地扩容128字节
原地扩容256字节
原地扩容512字节

 我们可以使用带参数的构造函数来解决这个问题:

class Stack
{
Public:
	Stack(size_t n = 4)//缺省参数,默认四个字节
	{
		if (n == 0)  //如果n为0,那就说明没传值,那就开辟默认的4字节空间
		{
			a = nullptr;
			top = capacity = 0;
		}
		else//否则就是传值了
		{
			a = (int*)malloc(sizeof(int) * n);//假如传了1000,那这里就是10000*4=4000,一下开辟4000字节的空间,就不会再扩容了
			if(a == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}

			top = 0;
			capacity = n;
		}
	}
}
int main()
{
	Stack st2(1000);//传1000
return 0;
}

 运行结果:

总结: 

析构函数

默认成员函数只会自定义类型的才会生成,会去自己找自定义类型的默认成员函数

比如我们的MYQueue

class MYQueue
{
public:
	
private:
	Stack  _Psuhpost;
Stack  _Poppost;
};

我们去调这个MYQueue

int main()
{


	MYQueue tr;

 它就会自己调用栈的构造函数和析构函数,因为栈也是自定义类型

自动调用构造函数

完成初始化

出了作用域自动调用析构函数

完成销毁

内联函数

 Inline

内联函数是为了替代宏函数而出来的。

下面用宏实现一个ADD宏函数:

为什么这个ADD宏函数要这么写,首先我们来看,假设这样写:

#define ADD(x,y)(x+y)

会有什么问题呢?

宏函数是直接替换了:

3|4+3&4=3|7&3

我们发现因为优先级问题,宏函数的直接替换导致结果偏离了我们的预设。

所以我们需要加括号保证优先级,但是这样太麻烦了,如果是一个ADD函数就不用这么麻烦:

 因为ADD函数不像ADD宏函数那样无脑替换,它会进行先把实参算完再拿过来给形参,把形参算完之后再返回。

但是函数的调用需要建立栈帧,假如我们需要调一万次ADD函数呢?那这个开销就比较大了,我们可以把ADD函数设为内敛函数:

普通函数:

 内联函数:

 内联不会展开,也就是可以把这个函数的功能拿过来直接用,而不用建立栈帧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孙鹏宇.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值