通俗易懂C++类和对象详解(一)什么是类?如何实现?

发布于:2023-01-24 ⋅ 阅读:(45) ⋅ 点赞:(0) ⋅ 评论:(0)

C++类和对象详解(一)

6000字详细介绍类和对象



0. 前言

  • 阅读本文所需的基础知识

    • C语言结构体
    • static和const修饰
    • 函数的传参
    • C++命名空间及作用域
    • 面向对象的编程思想

1. 类的引入

  • 从结构体到类

在C语言中,结构体中可以定义成员变量。

struct MyStruct {
	int a;
	int b;
};

创建结构体变量,通过 结构体变量名.结构体成员 可以访问其内部成员。

struct MyStruct st;
st.a = 1;
st.b = 2;

在C++中,定义结构体对象时,可以省去关键字struct,直接使用结构体名。

//C
struct MyStruct st;
//C++
MyStruct st;

以上结构体struct的定义,C++中更多用类class代替。


  • 类的定义

在C语言中,结构体中只能定义变量。在类中不仅能定义变量,还能定义函数

同结构体一样,类的定义为 class + 类名

class MyClass {
public:
	void function();
private:
	int i;
};	//类的定义

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

MyClass A;	//类的实例化

  • 访问限定符

C++使用访问限定符限定类中成员的访问,分为以下三种:

访问限定符 说明 访问权限
public 公有的 在类外可以直接访问
private 私有的 只能被类中的成员函数访问,类外无法直接访问
protected 保护的 能被类中和子类的成员函数访问,类外无法直接访问

一个类中可以有多个访问限定符标识,访问限定符的作用范围到下一个访问限定符或类定义结束}为止。

class MyClass {
public:
	void func1() {
		cout << "qwq\n";
	}
private:
	int a;
	char str[10];
public:
	static int i;
};

与结构体不同的是,结构体实例化的对象成员默认访问权限为public,而实例化的对象的成员访问权限默认private无法从外部直接访问
qwq

类定义的代码排版:三个访问限定符 public:private:protected: 与该类定义位置的 class 保持同一缩进且单独占一行,类中成员则向后缩进,如上所示。


  • 类的作用域

类定义了一个域名为类名的作用域,类中的成员都在这个作用域中。在类外访问类内成员时需要使用作用域解析操作符::

class A{
public:
	static void fun(){
		cout << "qwq\n";
	}
};
//在类外调用类A中的公有静态成员函数fun
A::fun();

通过::访问成员,属于从类外访问类内成员,因此只能访问public限定的成员,而不能访问privateprotected限定的成员。

关于static修饰成员的使用,在之后的章节会讲到。


2. 类对象在内存中的储存形式

  • 计算类对象的大小

类中同时存在成员变量和成员函数,那么类实例化的对象在内存中是如何储存的呢?以下面的类为例。

class A {
public:
	void fun1(){
		cout << 1;
	}
	void fun2(){
		cout << 2;
	}
private:
	int a;
	int b;
};

sizeof计算类的大小和类实例化的对象的大小:

A a1;
cout << sizeof(A) << endl;
cout << sizeof(a1) << endl;

输出结果:

8
8

可以发现,此时类的大小等于类中的成员变量大小之和,因此可以得出:类在实例化的时候,并没有包含类中的成员函数。

  • 对象在内存中的储存

qwq事实上,类实例化的对象,只会将类中成员变量按照结构体对齐规则储存,而类中的成员函数则保存在公共代码区,不占用对象的空间,如上图所示。

另外,如果是空类或者类中不存在非静态类型的成员,则这种类实例化的时候编译器会给一个字节来标识这个对象。


3. 类的成员函数

  • 成员函数的定义

成员函数可以定义在类中,也可以声明在类中定义在类外。

class A {
public:
	//类中声明
	void fun1();
private:
	void fun2();
	int _a;
	int _b;
};
//类外定义
void A::fun1() {
	cout << _a;
}
void A::fun2() {
	cout << _b;
}

需要注意的是,在类外部定义的函数,需要在函数名前加上类域名和作用域解析操作符::。类外部也可以定义类中用privateprotected限定声明的函数。


  • 成员函数的调用

成员函数的调用,可以使用对象.成员函数()的方式,如下:

class A {
public:
	void fun() {
		cout << 1;
	}
	void init(int a) {
		_a = a;
	}
private:
	int _a;
};
int main() {
	A a1;
	a1.fun();   //调用成员函数fun
	a1.init(3); //调用成员函数init
	return 0;
}

也可以用匿名对象的方式调用成员函数,如下:

class A {
public:
	void print() {
		cout << "qwq\n";
	}
};

int main() {
	A().print();
	return 0;
}

对象实例化时,通常要有对象的名字,比如A a1;。上面的代码中,A()是一个匿名对象,因为没有名字,所以之后就无法施用,其生命周期只有A().print();这一行。

  • 成员函数的传参 - this指针

在上面的代码中,对象a1是被定义在main函数中的,因此a1的成员变量_a也是储存在main函数的栈帧中的。但是在a1.init(3);中,对象a1中的成员变量_a在函数init中被改变了,从表面上看,对象a1并没有传参到init中,这是怎么回事呢?

通过 Visual Studio 2019 调试转到反汇编,可以发现在执行语句a1.init(3);时,不仅传了参数3,还传了对象a1地址
qwq
再查看函数void init(int a)的反汇编,可以发现它用一个名为this的指针接收了传过去的对象a1地址
qwq
可以发现,函数init内部其实是通过this指针修改了_a的值。
qwq
事实上,类的每个非静态成员函数中都隐藏了一个this指针,该指针指向当前调用的对象,被作为成员函数的第一个参数。

在函数void init(int a);中,实际的参数表为void init(A* const this, int a);。在传参时,a1.init(3)实际上传的是a1.init(&a1, 3)
qwq


  • const修饰的成员函数

在C++中,使用没有const修饰的指针,不能直接接收const修饰的常量地址。在这里插入图片描述如果要用指针接收变量的地址,则必须用const修饰指针变量。
在这里插入图片描述
同样的,类的成员函数中,第一个参数this指针默认是不被const修饰的,比如下面的类,它有一个隐藏的参数A* const this

class A {
public:
	void fun() {
//	void fun(A* const this) <-实际的形式
		cout << _a << endl;
	}
	int _a;
};

由于const*之后,所以并不能限制*this的修改。因此,如果向该函数中传入const修饰的A类型对象,则会报错:
在这里插入图片描述
要解决这个问题,我们必须用const修饰this指针,即const A* const this,但是this指针是一个隐藏的参数,无法直接在该参数上修饰,怎么办呢?

使用const在函数圆括号后修饰,即可修饰隐藏的this指针。表示该成员函数不能修改任何对象中的成员变量


class A {
public:
	void fun() const {
//	void fun(const A* const this) <-实际的形式
		cout << _a << endl;
	}
	int _a;
};


  • this指针的使用

this为空指针时,只要this不发生解引用,程序依然能正常运行:

class A {
public:
	void print() {
		cout << "qwq\n";
	}
private:
	int _a;
};

int main() {
	A* pa = nullptr;
	pa->print();
	return 0;
}

虽然pa是个空指针,但函数print并不是在对象中储存的,且传入的this指针没有发生解引用操作。因此以上代码能够正常编译运行。


this指针也可以作为返回值使用,观察下面代码中的add函数:

class A {
public:
	void init(int a = 0) {
		_a = a;
	}
	A& add() {
		++_a;
		return *this;
	}
	void print() {
		cout << _a << endl;
	}
private:
	int _a;
};
int main() {
	A a;
	a.init(0);
	a.print();
	a.add().add();
	a.print();
	return 0;
}

在语句a.add().add();中,因为this指针将对象本身作为返回值引用返回,所以a.add()的返回值可以作为对象再次调用add()

输出结果:

0
2

4. static修饰的成员

  • 静态成员变量

在C语言中,全局变量和用static修饰的静态变量在内存中都是储存在静态区中的。

int a = 0; //全局变量 储存在静态区

void fun(){
	int a = 10; //局部变量 储存在栈区
	int* p = (int*)malloc(sizeof(int));
	*p = 20; //在堆中开辟的空间 储存在堆区
	static int s = 30; //静态变量 储存在静态区
}

在C++中,成员变量的类型允许用static修饰为静态成员变量,静态成员变量也储存在静态区,不占用对象的内存空间。因此,静态成员变量是该类类型所有对象共享的,不属于某个具体对象

//在类中声明静态成员变量
class A {
public:
	static int _a;
private:
	int _i;
	static int _b;
};

静态成员变量必须在类外定义初始化,定义时需要指明类域,并使用作用域解析操作符::,不需要使用static修饰。仅在定义时,可以在类外初始化privateprotected限定的静态成员变量。

int A::_a = 10;
int A::_b = 20;

对于public修饰的成员变量,普通的成员变量可以使用对象访问,静态成员变量不仅可以使用对象访问,还以通过类域名和作用域解析操作符::访问。

class A {
public:
	static int _s1;
	static int _s2;
	int _i;
};

int A::_s1 = 0;
int A::_s2 = 0;

int main() {
	A a;
	//通过对象可以访问普通成员变量和静态成员变量
	a._i = 10;
	a._s1 = 20;
	//通过类域可以直接访问静态成员变量
	A::_s2 = 30;
//	A::_i = 40; //错误的 不能通过类域访问普通成员变量
	return 0;
}

如上,通过A::_s2不创建对象直接访问静态成员变量。


  • 静态成员函数

在C++中,不仅可以用static修饰成员变量,还能修饰成员函数。

静态成员函数与其他成员函数不同的是,其参数表中没有this指针,因此静态成员函数不能访问非静态成员变量,且和静态成员变量一样可以通过类域名和作用域解析操作符::访问。

class A {
public:
	static void print() {
		cout << _a << endl;
		
	//	cout << _i << endl;
	//	错误的 静态成员函数没有this指针,无法访问非静态成员变量
	}
private:
	static int _a;
	int _i;
};
int A::_a = 1;

int main() {
	A::print(); //通过类域直接访问静态成员函数
	return 0;
}

static成员总结:

成员变量 在内存中的储存位置 访问方式 初始化 是否共享
非静态 对象储存的位置 只能通过对象访问 只能通过对象初始化 每个非静态成员变量都是独立的
静态 静态区 可以使用类域访问 在类外全局初始化 该类的对象共享同一个静态成员变量
成员函数 调用方式 有无this指针 访问静态成员变量 访问非静态成员变量
非静态 只能通过对象调用
静态 可以使用类域调用 不能

5. 友元

  • 友元函数

由前面的知识可知,类外的函数无法访问类中privateprotected限定的成员变量。如果要让类外函数能访问,需要在类中声明该函数为友元函数,即在函数声明前加上关键字friend。此时该函数虽然不是类的成员函数,但能访问类的非public成员。

class A {
public:
	//声明友元函数
	friend void fun(A& a);
private:
	int _i;
};

void fun(A& a) {
	//在非成员函数中访问类的私有成员变量
	a._i = 1;
}

友元函数是定义在类外的普通函数,不属于任何类的成员,没有this指针,因此不能使用const修饰。它可以同时是多个类的友元,可以在类定义的任何地方声明,不会受到访问限定符的限制。

需要注意的是,友元函数会破坏类的封装,增大耦合度,所以不宜多用


  • 友元类

在一个类中,如果用friend修饰声明另一个类,则称另一个类是这个类的友元类。友元类使用 friend class + 类名 声明。

如下代码,BA的友元类,所以B中的成员函数可以访问A中的非public成员。

class A {
public:
	friend class B;
private:
	int _a;
};

class B {
public:
	void fun() {
		A a1;
		//因为B是A的友元类,所以可以直接访问A中私有的_a成员
		a1._a = 1;
	}
private:
	int _b;
};

友元类是单向的,以上的代码中,B中能访问A的非public成员,但是A中不能访问B的非public成员。

友元类不具有传递性,如下代码:BA的友元类,即B中能访问A的成员,CB的友元类,即C中能访问B的成员,但是CA没有友元关系,因此C中不能访问A中的成员。

class A {
public:
	friend class B;
private:
	int _a;
};

class B {
public:
	friend class C;
private:
	int _b;
};

class C {
public:
	void fun() {
		A a1;
	//	a1._a = 1;
	//  错误的 A和C之间没有友元关系
	}
private:
	int _c;
};

  • 内部类

如果一个类定义在另一个类的内部,则这个类天生就是另一个类的友元

class A {
public:
	//B定义在A的内部
	class B {
	public:
		void fun() {
			cout << _i << endl;
			A a1;
			//B是A的友元
			a1._a = 1;
		}
	};
private:
	int _a;
	static int _i;
};

由于B只是被定义A类中,因此计算sizeof(A)时,并不会计算B中的成员变量大小。


下一篇:C++类和对象详解(二)