C++初阶:类和对象

一.面向过程和面向对象

面向过程程序设计:

通过学习C语言,我们发现,面向过程的程序设计是一种自上而下的设计方法,以事件为中心,以功能为导向,分析出解决问题的步骤,按模块划分出程序任务并由函数实现,依次执行各函数,实现功能。其特征是以函数为中心,用函数来作为划分程序的基本单位,数据在过程式设计中往往处于从属的位置。                                                                                                                                                 面向过程的程序设计把数据和数据处理过程分离为相互独立的实体。当数据结构改变时,所有相关的处理过程都要进行相应的修改,每一种相对于老问题的新方法都要带来额外的开销,程序的可用性极差。特别是在大型项目中,面向过程的编程面临着巨大挑战。

面向对象程序设计:

面向对象程序设计描述的是客观世界中的事物,以对象代表一个具体的事物,把数据和数据的操作方法放在一起而形成的一个相互依存又不可分割的整体,再找出同类型对象的共有属性和行为,形成类,这一过程称为抽象。抽象是面向对象编程思想的本质,而类是其关键。类通过外部接口与外界发生关系,避免被外界函数意外改变,对象与对象之家通过消息进行通信,这样就保证了数据的独立性与安全性。封装,继承和多态是面向对象程序设计的三大特征。

小结:

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。         C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

二.类与对象

2.1.类和对象的关系

面向对象的编程思想力求在程序中对事物的描述与该事物在现实中的形态保持一致。为此,面向对象的思想中提出了两个概念:类和对象。其中,类是对某一类事物的抽象描述,对象表示现实中该类事物的个体。类是对多个对象共同特征的抽象描述。是对象的模板。对象用于描述实现中的个体,它是类的实例。

2.2.类的引用

在C语言中,结构体只能用于定义变量,而C++本身是兼容C的语法的,因此我们也可以在C++中使用关键字struct来定义只含变量的结构体:

//C语言
typedef struct ListNode_C
{
	struct ListNode_C* next;
	int val;
}LTNode;

//C++
struct ListNode_CPP
{
	struct ListNode_CPP* next;
	int val;
};

int main()
{

	LTNode st1;

	struct ListNode_CPP st3;
	ListNode_CPP st4;//在C++中,struct成为定义类的关键字,在对象声明中可以省略,但在C中不可省略

	return 0;
}

但是,在C++中,结构体内不仅可以定义变量,也可以定义函数:

//Stack既是类型又是类名
struct Stack
{
	void Init()
	{
		a = 0;
		top = capacity = 0;
	}

	void Push(int x)
	{
		//...
	}

	void Pop()
	{
		//...
	}

	int* a;
	int top;
	int capacity;
};

//上面结构体的定义,在C++中更喜欢用class来代替
int main()
{
	//法一:兼容c
	struct Stack st1;

	//法二:在c++中,Stack既是类名又是类型,前面的struct可以省略不写
	Stack st2;

	st1.Init();
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	return 0;
}

小结:

与C语言中定义结构体类似,C++中也可以通过struct关键字定义类。使用struct关键字定义的类与class定义的类的区别是:类中成员默认的访问权限不同

2.3.类的定义

面向对象程序设计的核心就是通过对象来反映现实事物,为了在程序中创建对象,必须首先定义对象的所属类。类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同特征和行为。类中可以定义数据成员和成员函数,数据成员用于描述对象特征,成员函数用于描述对象行为,其中数据成员也被称为属性,成员函数也被称为方法。类的定义形式如下所示:

class 类名
{
成员访问限定符:
	数据成员;
成员访问限定符:
	成员函数;
};

下面对类的定义语法进行简要说明:

  1. class是定义类的关键字;
  2. class 后是表示类名的标识符,为了做到见名知意,通常类名由若干单词构成,每个单词的首字符大写。类名和前面的class关键字需要用空格,制表符,换行符等任意的空白字符进行分隔;
  3. 类名后面要写一对大括号,类的成员要在其中说明。在说明成员时,通常使用成员访问限定符说明成员的访问规则;
  4. 右大括号后面的分号“;”表示类定义的结束。

类的两种定义方式:

1.声明和定义全部放在类体中。需要注意:成员函数如果在类中定义,编译器可能会(符合inline条件,即编译指令比较少)将其当成内联函数处理。

2.类声明放在.h文件中,成员函数定义放在.cpp文件中。需要注意:成员函数名前需要加类名::

小结:

一般情况下,更期望采用第二种方式。

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

访问限定符

访问限定符声明了类中各个成员的访问权限。C++中可用的访问限定符有public,protected,private三个。下面对三种访问限定符的属性进行说明:

public:

被public修饰的成员也称为公有成员,具有类外交互的能力,可以被该类的其他成员函数及类外的其他函数使用。

private:

被private修饰的成员称为私有成员,只能由类中的函数访问,不可通过该类的其他成员函数及类外的其他函数使用。之所以引用private是因为在面向对象思想中最重要的一个特点就是封装,类中的成员不应该被随意更改,防止成员存入一个不合理的数值,数据操作都应该在可控范围之内。

protected:

被protected修饰的成员称为保护成员,其访问权限介于私有和公有之间,本类的成员和该类的派生类可以访问,类外的其他函数不可以访问。

注意:

  1. 若没有指定成员的访问权限,通过class关键字定义的类中成员默认具有private属性,通过struct关键字定义的类中成员默认具有public属性;
  2. 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别;
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。

面试题:

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

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

封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。                                                                                     在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来
隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

2.5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};

//这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

2.6.类的实例化

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

类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。

一个类可以实例化出多个对象,实例化出的对象才能实际存储数据,占用物理空间,存储类成员变量。

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

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//类对象实例化:开辟空间
	Date d1;
	Date d2;

	//Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄
	//Date._year = 1;
	
	//d1._year = 1;//私有的,无法直接访问

	return 0;
}

2.7.类对象模型

案例:

class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}

private:
	char _a;
};

int main()
{
	A a;
	cout << "a的大小:" << sizeof(a) << endl;

	return 0;
}

问题:

类中既可以有成员变量,也可以有成员函数,那么一个类的对象包含了哪些成员?又如何计算一个对象的大小?

类对象的存储方式猜测

方式一:对象中包含类的各个成员

缺陷:

每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。 

方式二:代码只保存一份,在对象中保存存放代码的地址

方式三:只保存成员变量,成员函数存放在公共的代码段

问题:

对于上述三种存储方式,那计算机到底是按照哪种方式来存储的? 我们再通过对下面的不同对象分别获取大小来分析看下:

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

	}
};

//空类
class A3
{

};

int main()
{
	A1 a1;
	A2 a2;
	A3 a3;

	cout << "a1的大小:" << sizeof(a1) << endl;
	cout << "a2的大小:" << sizeof(a2) << endl;
	cout << "a3的大小:" << sizeof(a3) << endl;

	return 0;
}

运行结果:

通常我们会认为,每个对象都要为自己的数据成员和成员函数分配空间,但事实并非如此。每个对象的数据成员描述的是本对象自身的属性,例如汽车对象,a汽车是红色,b汽车是白色,因此在创建对象时应该为每个对象分配一块独立的内存来存储数据成员值,与C语言中的普通局部变量一样,类中的普通数据成员也被分配在栈中。但是成员函数描述的是对象执行的动作,每个对象都应相同,为每个对象的成员函数也分配不同空间必然造成浪费。因此C++中用同一段空间存放同类对象的成员函数代码,每个对象调用同一段代码。

结论:

一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。 

扩展:结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处;
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的对齐数为8;
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍;
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

2.8.this指针

通过前面介绍的知识我们了解到,类中每个对象的数据成员都占用独立空间,但成员函数是共享的,可是各个对象调用相同的函数时,显示的是对象各自的信息,这是如何实现的呢?

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 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++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

我们将上述案例的代码进行改写,可以发现this指针确实是指向当前对象的。

class Date
{
public:
	void Init(int year, int month, int day)
	{
		cout << this << endl;
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//对象调用:.
	Date d1;
	Date d2;

	d1.Init(2022, 2, 2);
	cout << "&d1:" << &d1 << endl;

	cout << "------------" << endl;

	d2.Init(2022, 2, 2);
	cout << "&d2:" << &d2 << endl;

	return 0;
}

运行结果:

this指针的特性

  1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。this指针是被const常所修饰的,为指针常量,指针本身的指向是不可修改的,但可以通过解引用的方式来修改指针所指向的内容;
  2. 只能在“成员函数”的内部使用;
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针;
  4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

面试题:

this指针存在哪里?

答:很多人认为this指针存在对象里面的,但其实并不是。C++规定,当一个成员函数被调用时,系统自动向它传递一个隐含的参数,也就是this指针是作为形参而存在的。而形参和函数中的局部变量都是存在函数栈帧里的,所以 this 指针可以认为是存在栈的。需要注意的是:VS下为了提高效率,编译器会对this指针的存储位置进行优化:将this指针存储在寄存器中,以此来提高读取和访问数据的效率。

this指针可以为空吗?

案例一:下面程序编译运行结果是?

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

private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();

	return 0;
}

运行结果:

我们本以为对一个空指针进行解引用,会导致程序崩溃,但是并没有。当我们对this指针进行查看时,可以发现:

可以看到this指针所接收到的地址为空,因为我们在外界调用这个函数的对象就是一个指向为空的指针。成员函数的地址是存放在公共代码区的,并不是存放在对象中。因此调用成员函数Print时,并不会去访问指针p所指向的空间,这里只会把指针p传递给隐含的this指针,所以也就不存在空指针的解引用。

案例二:下面程序编译运行结果是?

class Date
{
public:
	void Init(int year, int month, int day)
	{
		cout << this << endl;
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	void func()
	{
		cout << this << endl;
		cout << "func()" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//空指针问题
	
	//指针调用:->
	Date* ptr=nullptr;
	ptr->Init(2022, 2, 2);//运行崩溃:要用this去解引用成员变量

    ptr->func();//正常运行:不涉及this解引用
	(*ptr).func();//正常运行:不涉及this解引用

	return 0;
}

调试分析:

当我们对程序进行调试时,Init初始化函数会发生空指针异常。因为在Init函数里访问成员变量时会补充上this->_year。由上一个案例可知,此时的隐含指针this是一个空指针,而对一个空指针进行解引用则会导致程序运行崩溃。

补充:

在使用指针ptr去调用成员函数时,使用->*均可。同时,我们可以在函数中使用this指针访问成员变量或调用成员函数:

(*this).成员变量或函数
this->成员变量或函数

由于this指针是指向当前对象的指针,所以我们可以在函数中把this指针当参数使用,或从函数中返回,用作返回值,形式如下所示:

return this;
return *this;

凡是想在成员函数中操作当前对象,都可以通过this指针完成。

小结:

this指针是存放在栈区的,它只是成员函数的一个隐式形参,但是却不和成员函数一样存放在公共代码区。成员函数只是编译器将其解析后的汇编指令存放在公共代码区,而对于函数内部的形参和临时变量依旧存放在栈区;
this指针可以为空,如果只是传递空对象地址但是并没有对其进行解引用,则不会引发异常。但若是在成员函数内部访问成员变量的话,无论你有无给出this->,都会因为对空指针进行解引用而导致程序异常。

三.构造函数和析构函数

从前面学到的知识可以发现,实例化了一个类的对象后,若想为对象中的数据成员赋值,需要直接访问成员或调用设置成员值的函数。若想在实例化对象的同时就为对象的数据成员进行赋值,可以通过调用构造函数的方法来实现。与之对应的,如果想在操作完对象之后,回收对象资源,可以通过调用析构函数来实现。构造函数和析构函数是类的特殊成员,它们分别在对象创建和撤销时被自动调用。

3.1.构造函数

构造函数是类中特殊的成员函数,用于初始化对象的数据成员。

3.1.1.构造函数概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

其语法格式如下:

class 类名
{
public:
	构造函数名称(参数表)
	{
		函数体
	}
private:
	数据成员;
};

构造函数的定义语法规定:

  1. 构造函数名与类名相同;
  2. 构造函数名前面没有返回值类型声明;
  3. 构造函数中不能通过return语句返回一个值;
  4. 通常构造函数具有public属性。

3.1.2.构造函数特征

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

特征一:构造函数名与类名相同;

特征二:构造函数名前面没有返回值类型声明;

特征三:对象实例化时编译器自动调用对应的构造函数;

特征四:构造函数可以重载;

通常我们希望能在对象创建时为数据成员提供有效初值,通过定义带参数的构造函数可以实现这样的功能。此外还可定义多个具有不同参数的构造函数,实现对不同数据成员的初始化。定义多个构造函数也就是构造函数的重载。

class Stack
{
public:
	//无参构造函数
	Stack()
	{
		cout << "不带参数:Stack()" << endl;
		_a = nullptr;
		_size = _capacity = 0;
	}

	//带参构造函数
	Stack(int n)
	{
		cout << "带参数:Stack()" << endl;

		_a = (int*)malloc(sizeof(int) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = n;
		_size = 0;
	}

	void Push(int x)
	{
		//...
		_a[_size++] = x;
	}

	//...

	bool Empty()
	{
		//...
		return _size == 0;
	}

	int Top()
	{
		return _a[_size - 1];
	}

private:
	//成员变量
	int* _a;
	int _size;
	int _capacity;
};


int main()
{
	//对象实例化时编译器自动调用对应的构造函数
	//调用无参构造函数
	Stack st;
	//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	//Stack st();//error

	//调用带参构造函数
	Stack st(4);

	st.Push(1);
	st.Push(2);
	st.Push(3);
	st.Push(4);

	return 0;
}

注意:

如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。

特征五:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;

只要类中定义了一个构造函数,C++将不再提供默认的构造函数。如果在类中定义的是带参构造函数,创建对象时想使用不带参数的构造函数,则需要再实现一个无参的构造函数,否则编译错误。如下所示:

c
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值