C++类与对象(一)

目录

1.面向过程和面向对象

2.类的引入

3.类的定义

4.类的访问限定符及封装

4.1访问限定符

4.2 封装

5.类的作用域

6.类对象模型

7.this指针


1.面向过程和面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

这只是对于面向过程和面向对象的一个粗浅的认识,我们要在后面的C++的学习过程中慢慢理解这个概念。

2.类的引入

在C语言中,有一种自定义类型叫结构体,但是结构体中只能定义变量,C++在C语言结构体的基础上,对struct进行了升级,在C++中,结构体不仅可以定义变量,还可以定义函数,讲struct升级成了类,同时,他也兼容了C语言中struct的所有用法。在C++类中,我们把成员变量称为属性,把成员函数称为方法。

那么把函数直接定义在类中有什么好处呢? 首先,简化了函数的名字,我们之前用C实现数据结构,每一个函数名都是由数据结构类型和操作一起组成的,这是为了防止不同数据类型放在一起用的时候防止命名冲突,而我们如果把函数定义在类里面,我们通过类变量就能直接调用到类里面的函数,这样命名也间接了很多。其次,我们还不用像C语言一样复杂的传参了,比如C语言链表头插既要传头结点指针有要传数据,而在类中的方法,参数就只需要传插入的数据就行了,因为方法和变量都定义在类里面,通过类里面的函数就能直接操作成员变量。 

比如,我们之前学的顺序表可以这样定义

struct SeqList
{
	void Init()
	{
		a = (int*)malloc(sizeof(int) * 4);
		assert(a);
		capacity = 4;
		size = 0;
	}
	void Push(int x)
	{

		a[size] = x;
		size++;
	}
	void Destroy()
	{
		free(a);
		a = nullptr;
		size = 0;
		capacity = 0;
	}

	int* a;
	int size;
	int capacity;
};

我们定义简单的初始化销毁和尾插函数在类里面,

	SeqList sl;
	sl.Init();
	sl.Push(4);
	sl.Push(3);

	sl.Destroy();

这时候如果我们用这个类类型创建一个对象,创建完之后我们只需要调用他的初始化函数就能完成初始化了,而不用想C语言中的实现一样,还要传它的指针。

而我们的尾插也是很简单的,通过这个对象调用尾插函数,传一个数据就行了,

销毁的时候也只需要通过这个对象调用销毁函数就完成了

我们发现,这样对顺序表进行各种操作比C语言中的实现要方便得多,而调用成员方法我们也是用到结构体访问成员的操作符(.或->)。

而且我们发现了一个于C语言不一样的地方,我们定义这个结构体或者说类的时候,我们可以省略struct ,直接用结构体名字来定义对象,这是因为在C++的编译器中把他当成了一个类,用类类型来创建变量是不用加前面的关键字的。

在上面我们用的还是C语言的结构体那一套,只是C++对结构体进行了升级,但是在C++中,我们定义一个类更喜欢用 class这个关键字而不是struct 

3.类的定义

虽然C++中兼容C语言的结构体,但是对于大多数人来说,对于类的定义我们还是更喜欢用class来定义,class是C++中用来定义类的关键字,

class classname
{
	//类体:类成员函数 + 类成员变量
};

在类的定义里面,没有规定成员函数和成员变量的位置,我们可以按任意顺序定义变量和函数,因为类是一个整体,编译器在搜索类的时候是不分上下前后顺序的,会直接搜索整个类。但是我们更建议把函数定义在一起,变量定义在一起,这样我们去找成员变量的时候更加方便。

比如上面的顺序表我们用class定义的话:


class SeqList
{

	void Init()
	{
		_a = (int*)malloc(sizeof(int) * 4);
		assert(_a);
		_capacity = 4;
		_size = 0;
	}
	void Push(int x)
	{
		_a[_size] = x;
		_size++;
	}
	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_size = 0;
		_capacity = 0;
	}

	int* _a;
	int _size;
	int _capacity;
};

在这个大花括号中间的称为类域,每一个类都是一个天生的类域,但是我们不能直接访问类域的变量,因为类也是定义一个类型,跟结构体类似,它只是一种自定义类型,而不是一个具体的对象,类里面的成员只是声明,生命是没有空间的,只有实例化对象之后才会开辟空间。

我们只有通过用这个类创建的实体才能够访问到里面的变量或者调用其中的函数,为什么要创建对象之后才能调用成员函数这一点我们在这篇文章的后半部分 this指针哪里会讲到。 类域不像我们之前学的命名空间,命名空间里面的是变量的定义,开辟空间了,而类域里面的变量则只是一个声明,还没开辟空间,要在实例化对象的时候才开辟空间。(在C++中我们一般把类类型的变量称为对象或者实体,用这个类类型创建对象叫做实例化对象)。

类的定义方式除了上面的将函数的声明和定义全部放在类中,这种方式要注意的点是,编译器可能会将其当成内联函数处理,我们也可以讲inline,但是就算我们不加inline 编译器也会默认把它当成内联函数。

第二种定义方法就是成员函数的声明和定义分离,我们可以把类的声明放在.h文件中,类中的成员函数只声明,而把函数的实现放在.cpp中。但是这时候要怎么表示这个函数是这个类的成员函数呢?因为我们可能会有多个类,类的成员函数可能重名,这在C++中是很常见的,这就得用到我们讲的域作用限定符 : :  了,这样一来编译器就能区分是哪个类的成员函数。

 定义的时候指定类域是放在函数名前,而不是在返回类型之前。同时我们要知道,成员函数在声明和定义上和普通的函数没区别,也是能用缺省参数的,这时候缺省参数也是只在声明中给,定义中就不要写缺省值了。我们顺便把前面的 init 函数改成有参数的类型,给一个缺省值,这样更方便我们使用顺序表

.h
	void Init(int capacity = 4)

.cpp

void SeqList::Init(int capacity)
{
	_a = (int*)malloc(sizeof(int) * capacity);
	assert(_a);
	_size = 0;
	_capacity = capacity;
}

这里我们要注意类域与域作用限定符的位置,一定是在函数名前面,因为返回类型是不分在哪个域的,我们要指定的是函数的作用域。

在这里我们又发现一个与C语言的不同的地方,就是我们在成员变量的前面都加上了一个前杠 _ ,这是一种命名习惯,有的人也喜欢在成员名前面加 m(member)_ 或者在成员名后面加 _ ,这是为了区分类的成员变量和函数的形参。

我们可以注意到什么的初始化函数,我们的参数就是 capacity ,如果类成员变量名也是capacity的话,我们就分不清那个是成员变量那个是形参了,所以我们用 _ 来区分这两个变量。

4.类的访问限定符及封装

4.1访问限定符

那么按照我们上面的的类的定义,我们实例化一个对象之后就能直接调用里面的成员函数了吗?这时候当我们定义一个对象,想要去调用它的初始化函数时,就会出现以下问题

这是为什么呢?这就要涉及到我们这个地方要讲的访问限定符了。

访问限定符有三个 1. public(公有) 2. protect (保护)3. private(私有)

这三个访问限定符限制的是类外面的而访问 ,public 修饰的成员在类外可以直接被访问,protect和private修饰的成员在类外不能被直接访问。

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现位置,如果后面没有访问限定符了,作用域就到类域结束。这一点不难理解


class A
{
public:
	void Init();
	void Destroy();

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

比如我们这里的public的作用域就是从 public 出现到 private的的前面,而private的作用域则是从private到类结束,也就是 } 。

那么我我们上面的问题是怎么出现的呢?为什么之前用struct定义的类就没有出现这样的情况呢?因为class的默认访问权限是private,而struct 的默认访问权限是public,因为struct要兼容C语言,我们在C语言都是直接在外面访问结构体成员变量的。

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何限定符上的区别,这就是为什么我们上面的报错是在编译期间就报错了而不是程序执行期间报错。 

同时struct也是可以设置访问限定符的。

了解了访问限定符,我们就能解决上面的问题了,我们只需要将成员函数都设置成共有的就行了。但是成员变量要不要设置成共有的呢?请看我们下面的封装

4.2 封装

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

并不是说只有这三个特性,而是这三个特性最出名,最具代表性

封装是一种管理行为,C语言中,数据和函数是分开的,我们可以直接去访问数据,也可以通过调用函数来访问数据,这是一种很自由的方式,但是过于自由却可能带来危险,数据是裸露在外界的,任何人都可以对其操作,这是不安全的。 在C++中,把数据和方法都放进了类里面,在类里面用访问限定符控制,共有的成员在类外可以直接访问,而私有的成员就只能调用类里面共有的方法来访问,这样用起来更加规范,更方便管理。我们并不是说不让用户从类外访问数据,而是要用更加规范的方式来访问,这就是封装的意义。

封装就是将数据和操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,仅对外公开接口来和对象进行交互。

这时候我们就能想明白了,为什么数据都要设置成 private 了。

5.类的作用域

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

我们可以这样理解 命名空间是一道现实的强,墙里面是定义的变量,翻过这个墙外面就能访问到里面的变量。而类域也是一道墙,但是他是一道虚拟的强,因为类相对于只是一张图纸,我们只有用这个图纸去创建了对象之后,开辟空间之后才能去翻过这道墙去访问里面的成员。

6.类对象模型

既然类有成员函数和成员变量,那么类的大小该如何计算呢?或者说用这个类创建的对象的大小是多大呢?

我们可以思考一个问题,我们知道类是一种自定义类型,每一个对象的成员变量都是独立的,都存储在对应的对象的空间里,但是他们的成员函数呢?每一个对象中都要存一份成员函数吗?成员函数都是一样的,不同的对象调用成员函数的时候都是调用的同一个成员函数,这时候我们还有必要每一个对象里面都存一份吗?这时候就有三种方法来表示一个类:1.每一个对象既存储成员变量也存储成员函数,这种方法我们说了没有必要,太浪费空间了。 2. 将类的成员函数存到一个公共的区域,建一个表,然后每一个对象都存储这个表的地址 以及各自的成员变量,这时候比起第一种方法就节省了很多空间,只存储一个地址 。3. 也是把类的函数存储在公共的区域,但是每一个对象只存储各自的成员变量,当要调用成员函数的时候由编译器去找这个函数。为什么编译器能找到这些函数呢?这些函数就是编译器放在这个地方的,编译器肯定是有方法找到调用的。公共的区域指的是代码段。这时候我们采用第二种还是第三种呢?既然我们存不存地址都能找到函数并调用,那么不存的话还能节省空间,何乐而不为呢?

所以,每一个对象里面都只存储成员变量,而不存储成员函数的地址。

类的大小有没有内存对齐?

在结构体章节我们就说过,存在多个不同类型的变量的集合是要内存对齐的,这样能提高内存访问效率。

对于类,我们说了类中也只存成员变量,这时候他的内存对齐就跟结构体一样了,求它的大小并不难,比如上面的顺序表,我们只有一个指针和两个整型,所以只需要十二个字节(x86环境)。

但是有一种特殊情况,比如是空类或者类中只有成员函数而没有成员变量时,这时候如果按照上面的规则去计算的话内存就是0。

class A
{

};

class B
{
	void Init();
};

这时候就很难搞了,它的大小是0的话,那么意思就是开辟0个字节,不就是不开辟空间了吗?难道这些对象就是空气了?那么通过这个对象去调用成员函数的时候,他都没有空间,没有载体,怎么调用呢? 这时候,C++规定,这些空类或者只有成员函数的类的大小都是 1 ,这一个字节是为了占位,标识对象的存在,不存储有效数据。

7.this指针

我们用类的成员函数的时候是不是会有一个疑问?


void SeqList::Init(int capacity)
{
	_a = (int*)malloc(sizeof(int) * capacity);
	assert(_a);
	_size = 0;
	_capacity = capacity;
}
void SeqList:: Push(int x)
{
	_a[_size] = x;
	_size++;
}
	sl1.Init();
	sl2.Init();

	sl1.Push(1);

	sl2.Push(2);

我们可以看到,我们定义函数的时候并没有传对象或者对象的地址啊,那么像上面的操作,它是如何知道我们要对哪个对象操作的呢?

这就是因为C++的一个隐藏的 this 指针,这个this是一个关键字。每一个成员方法的参数都会有一个隐藏的 this 指针,并且这个this指针是第一个参数。

这个this指针就是调用这个成员方法的对象的地址,比如上面的 sl1.Push,这时候Push的this指针就是sl1的地址,谁调用函数,this指针就是谁的地址。这就能解释了我们前面说的为什么不能直接通过类来调用成员函数而是要通过对象来调用,因为成员函数的参数列表里面是有一个隐藏this指针的,而类类型是没有实体的,也就没有地址传给this,只有定义的对象才有地址。

那么我们能不能自己显式地传this指针呢?答案是不能的。这是编译器自动干的事,每次调用成员函数编译器都会自动传地址。如果我们显式地写在函数声明或定义的参数列表中,编译器会报错:

这表明编译器已经隐式地对这个this指针参数进行声明了。传参数也不能传,编译器会报错函数调用的参数过多。

虽然我们不能自己传this指针,但是我们是可以在函数中使用this指针的,比如初始化的函数

void SeqList::Init(int capacity)
{
	this->_a = (int*)malloc(sizeof(int) * capacity);
	assert(this->_a);
	this->_size = 0;
	this->_capacity = capacity;
}

但是这种写法和上面的写法是完全等价的,因为就算我们不去解引用this指针,编译器在访问成员的时候也会自动去解引用。

那么有一个问题?这个this指针是存在哪里的呢?

首先可以排除的是,他肯定不是存在对象里面的,因为我们前面用sizeof求对象大小的时候是没有这个指针的空间的。我们说了,这个this指针是编译器自动传给成员函数的一个参数,只有去调用成员函数的时候才会有this指针这个东西,那么函数参数显而易见是存在栈区的,每次调用函数就传this,调用完函数销毁就销毁。(vs编译器进行了优化,把this指针存在了ecx寄存器中)。

那么我们来看一段程序


class A
{
public:
	void Print()
	{
		cout << "Print" << endl;
	}

	void PrintA()
	{
		cout << _a << endl;
	}

private:int _a;
};

int main()
{
	A* pa = nullptr;
	pa->Print();
	pa->PrintA();
}

 

我们看到这段程序编译是没有问题的,那么这两次函数调用有问题吗?

首先我们看Print函数,我们发现他的调用是没有问题的,为什么呢?pa明明是空指针,而我们用空指针->成员函数的方法却没有问题呢?  这里我们就要理解前面讲的知识了额,我们前面说过,成员函数并不是存在对象里面的,而是存在公共的代码段中,那么我们调用成员函数的时候需要对pa解引用吗?不要去直接就能去找到这个函数的地址,我们并不需要通过对对象解引用来找到函数,所以这里虽然写的是 -> ,但是实际上编译器并没有对 pa 解引用。

但是为什么当我们去调用PrintA函数的时候就执行错误了呢? 这里我们就要想到刚刚的this指针,编译器自动传了一个this指针,我们在函数中访问对象都是通过对 this 指针解引用来访问的,上面的Print函数,我们并没有访问对象的成员变量,而是只打印了一个字符串,所以没有问题。 而PrintA函数中,我们要打印 _a , 而_a要通过解引用this来访问到,这里的 this 就是pa ,而pa是空指针,对空指针的解引用就会执行错误。导致错误。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值