『C++』类与对象(上)

面向过程和面向对象

什么是面向过程的程序设计

  • 面向过程的程序设计反映的是事物在计算机中的实现方式,而不是事物在现实生活中的实现方式。程序设计者必须把现实生活中的实现方式转化为在计算机中的实现方式。
  • 面向过程的程序设计中,程序设计者必须指定数据在计算机中的表现形式,以及怎样对这些数据进行操作,以得到预期的结果
  • 程序设计者不仅要考虑程序要做什么,还要解决怎么做的问题,要根据程序要做什么的要求,具体设计出计算机执行的每一个具体的步骤,写出一个个语句,安排好它们的执行顺序。

什么是面向对象的程序设计

  • 面向对象的程序设计思路和人们日常生活中处理问题的思路是相似的。在自然世界和社会生活中,一个复杂的事物总是由许多部分组成的。譬如,一辆汽车是由发动机、底盘、车身和轮子等部件组成的;一套住房是由客厅、卧室、厨房和卫生间等组成的;一个学校是由许多学院、行政科室和学生班级组成的。
  • 当人们生产汽车时,并不是先设计和制造发动机,在设计和制造底盘,然后设计和制造车身和轮子,而是分别设计和制造发动机、底盘、车身和轮子,每一种部件分别用来实现不同的功能。在制造出所有的部件后,把它们组装到一起,就成为一辆汽车。在组装时,各部分之间有一定的联系,以便协同工作,例如装好传动系统,接通电路、油路。驾驶员踩油门,就能调节油路,控制发动机的转速,驱动车轮转动。

这就是面向对象的程序设计的基本思路

C与C++

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

蛋炒饭和盖浇饭的区别

  • 可以将面向过程理解为做一份蛋炒饭
  • 面向对象理解为做盖浇饭

类与对象

客观世界中任何一个事物都可以看成一个对象。对象可大可小,任何一个对象都应当具有两个要素,即属性和行为。对象是由一组属性和一组行为构成的。
在程序设计方法中,常用到抽象。其实抽象这一概念并不抽象,而是很具体的,人们对之是司空见惯的。例如,我们常用的名词“人”,就是一种抽象。因为世界上只有具体的人,如张三,李四,王五。把所有国籍为中国的人归纳为一类,称为中国人。这就是一种抽象。再把中国人、美国人、日本人等所有国家的人抽象为人。抽象的作用是表示同一类事物的本质。
类是对象的抽象,而对象则是类的特例,即类的具体表现形式。
在这里插入图片描述

类的引入

C语言中,我们知道结构体中可以定义变量,那么在结构体中可以定义函数吗

代码演示

#include <stdio.h>

struct Student{
	char name[1024];
	int age;

	void stuTest(){
		printf("I am a student!\n");
	}
};

int main(){

	struct Student stu;
	stu.stuTest();

	return 0;
}

运行结果

[sss@aliyun struct]$ gcc C_struct.c 
C_struct.c:7:16: error: expected ‘:’, ‘,’, ‘;’, ‘}’ or ‘__attribute__’ before ‘{’ token
  void stuTest(){

将文件改为C++

[sss@aliyun struct]$ mv C_struct.c C_struct.cpp
[sss@aliyun struct]$ g++ C_struct.cpp -o C_struct
[sss@aliyun struct]$ ./C_struct 
I am a student!

结论

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数,上面的结构体定义,在C++中更喜欢用class来替代
注意:struct定义的类成员未说明public或private默认全部是public(出于对C语言的兼容)。
class定义的类成员未说明public或private默认全部是private

类的定义

class ClassName{
	// 类体:由成员函数和成员变量组成。
};	// 分号!

class为定义类的关键字ClassName为类的名字{}中为类的主体,注意类定义结束时后面分号
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数

类的两种定义方式

在这里插入图片描述

  • 方法一:声明放在.h文件中,类的定义放在.cpp文件中
  • 方法二:声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  • 注意:类中定义的函数,默认是内联函数

为什么要将头文件源文件分离

可以有效的保护源代码,给别人头文件和一个动态/静态库就可以使用。
方便缕清工程的基本框架

类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
在这里插入图片描述

访问限定符说明

  • public修饰的成员在类外可以直接被访问。
  • protected和private修饰的成员在类外不能直接被访问。
    访问权限作用域从该访问限定符出现的位置直到下一个访问限定符出现时为止
  • class的默认访问权限位privatestructpublic(因为要兼容C)。
    注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

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

C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct的成员默认访问方式是public,class的成员默认访问方式是private。

代码验证

#include <iostream>
using namespace std;

struct Sss{
	int meng = 916;
};

int main(){
	Sss s;

	cout << s.meng << endl;

	return 0;
}
运行结果
[sss@aliyun struct]$ g++ C++_struct.cpp -o C++_struct -std=c++11
[sss@aliyun struct]$ ./C++_struct 
916

将struct改为class

#include <iostream>
using namespace std;

class Sss{
	int meng = 916;
};

int main(){
	Sss s;

	cout << s.meng << endl;

	return 0;
}
运行结果
[sss@aliyun struct]$ g++ C++_class.cpp -o C++_class -std=c++11
C++_class.cpp: In function ‘int main()’:
C++_class.cpp:5:13: error: ‘int Sss::meng’ is private
  int meng = 916;
             ^
C++_class.cpp:11:12: error: within this context
  cout << s.meng << endl;

封装

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

什么是封装呢?

封装本质上是一种管理
C++从两方面实现封装。首先将数据和方法都装到类里面,方便管理。再使用访问限定符加以限定。
可以对一个对象进行封装处理,把它的一部分属性和功能对外界隐蔽,也就是说从外界是看不到的、甚至是不可知的。例如:录像机里有电路板和机械控制部件,但是外面是看不到的,从外面看它只是一个“黑箱子”,在它的表面有几个按键,这就是录像机与外界的接口,人们不必了解录像机里面的结构和工作原理,只须知道按下某一按键就能使录像机执行对应的操作。也就是说,把对象的内部实现和外部行为分割开来
封装性指两方面的含义

  • 一是将有关的数据和操作封装在一个对象中,形成一个基本单位,各个对象之间相对独立,互不干扰
  • 二是将对象中某些部分对外隐蔽,即隐蔽其内部细节,只留下少量接口,以便与外界联系,接收外界的消息。这种外界隐蔽的做法称为信息隐蔽。信息隐蔽还有利于数据安全,防止无关的人了解和修改数据

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员,需要使用::作用域解析符指明成员属于哪个类域。
在这里插入图片描述

类的实例化

用类类型创建对象的过程,称为类的实例化
类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量
做个比方,类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类知识一个设计,实例化出的对象才能实际存储数据,占用物理空间。
在这里插入图片描述
在这里插入图片描述

类对象模型

如何计算类对象的大小

class Sss{
	public:
		void sssDisplay(){
			cout << s << endl;
		}
	public:
		char s = 0;
};

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

类对象的存储方式猜测

对象中包含类的各个成员

用类去定义对象时,系统为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数代码(指经过编译的目标代码)分配存储空间。如果用同一个类定义了5个对象,则为每一个对象的数据和函数代码分别分配存储单位,并把它们封装在一起。
在这里插入图片描述
缺陷:每个对象中的成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

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

同一类的不同对象中的数据成员的值一般是不一样的,而不同对象的函数的代码是相同的,不论调用哪一个对象的函数代码,其实用的都是同样内容的目标代码,在调用各对象的函数时,都去调用这个共用函数代码。
在这里插入图片描述

上述两种存储方式,计算机究竟按照哪种方式存储的呢?

下面看一段代码
#include <iostream>
using namespace std;

class Sss{
	public:
		void sssDisplay(){
			cout << s << endl;
		}
	public:
		char s = 0;
};

int main(){
	cout << sizeof(Sss) << endl;

	return 0;
}
运行结果
[sss@aliyun size_of_class]$ !g++
g++ class_size.cpp -o class_size -std=c++11
[sss@aliyun size_of_class]$ ./class_size 
1
若将char s = 0;改为int s = 0;结果如下
[sss@aliyun size_of_class]$ !g++
g++ class_size.cpp -o class_size -std=c++11
[sss@aliyun size_of_class]$ ./class_size 
4
若将int s = 0;改为double s = 0;结果如下
[sss@aliyun size_of_class]$ !g++
g++ class_size.cpp -o class_size -std=c++11
[sss@aliyun size_of_class]$ ./class_size 
8

结论:一个类的大小,实际就是该类中“成员变量”之,当然不是简单的相加,也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类1个字节来唯一标识这个类。

#include <iostream>
using namespace std;

class Sss{
};

int main(){
	cout << sizeof(Sss) << endl;

	return 0;
}
[sss@aliyun size_of_class]$ !g++
g++ class_size.cpp -o class_size -std=c++11
[sss@aliyun size_of_class]$ ./class_size 
1

为什么空类的大小要设置为1?

实例化的原因,空类同样可以被实例化每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后再内存中得到了独一无二的地址,所以空类所占的内存大小是1个字节。

代码演示

#include <iostream>

class Empty{
};

int main(){

	Empty e1;
	Empty e2;
	Empty e3;

	std::cout << "&e1: " << &e1 << std::endl;
	std::cout << "&e2: " << &e2 << std::endl;
	std::cout << "&e3: " << &e3 << std::endl;

	return 0;
}

运行结果

[sss@aliyun this]$ g++ empty_class.cc -o empty_class
[sss@aliyun this]$ ./empty_class 
&e1: 0x7ffdc57793af
&e2: 0x7ffdc57793ae
&e3: 0x7ffdc57793ad

结构体内存对齐

  • 第一个成员在与结构体偏移量为0的地址处
  • 其他成员变量对齐到某个数字(对齐数)的整数倍地址处
  • 注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值。VS中默认的对齐数为8gcc中的对齐数为4。可以通过* **#pragma pack(n)**将对齐数设置为n。
  • 结构体总大小为:最大对齐数(所有变量类型的最大者与默认对齐数中的较小值)的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

为什么要进行内存对齐

  • 现代计算机体系结构中,CPU每次读写内存中的数据,都是按字为一个块来操作的。这个块的大小取决于有多少根数据总线,对于X86架构,系统是32位的,数据总线和地址总线都是32根,所以最大寻址空间为4G,每次读取的块大小为32位即4个字节
  • 既然如此,如果变量在内存中存储的时候也按这样的规则进行对齐,就可以加快CPU读写内存的速度,当然也就提高了整个程序的性能。虽然当今CPU的处理数据速度(逻辑运算等,不包括取址)远比内存访问的速度快,程序的执行速度的瓶颈往往不是CPU的处理速度不够,而是内存访问的延迟,虽然如今CPU中加入了高速缓存用来掩盖内存访问的延迟,但是如果高密集的内存访问,延迟还是无可避免的,内存地址对齐会给程序带来很大的性能提升。

举例说明
在这里插入图片描述
方案一和方案二的存储方式,哪个读取比较快?(地址仅为方便理解,不要过分纠结。)
显然是方案二
分析
方案一,CPU为了读取这个int比那两的值,需要先后读取两个块,分别是0x0000 0000~0x0000 0003和0x0000 0004~0x0000 0007,然后通过移位等一系列的操作来得到,在这个计算的过程中还有可能引起一些总线数据错误。但是CPU对方案二中int型数据的读取只需要读取一个块0x0000 0000~0x0000 0003。明显快了很多。

this指针

  • 前面提到过,每个对象中的数据成员都分别占有存储空间,如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同的对象都调用同一个函数的目标代码
  • 那么,当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的成员呢

this指针的引出

#include <iostream>
using namespace std;

class Date{
	public:
		void dateDisplay(){
			cout << year << "-" << month << "-" << day << endl;
		}
		void dateSet(int y, int m, int d){
			year = y;
			month = m;
			day = d;
		}
	private:
		int year;
		int month;
		int day;
};

int main(){
	Date d1, d2;
	
	d1.dateSet(2019, 4, 27);
	d2.dateSet(1995, 9, 19);

	d1.dateDisplay();
	d2.dateDisplay();

	return 0;
}

对于上述类,有这样一个问题:

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

this指针的特性

  • this指针的类型:类类型* const
  • 只能在“成员函数”的内部使用。
  • this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  • this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
    在这里插入图片描述

this指针存在哪里?可以为空吗?

this指针既然是作为隐含参数来传递的,那么理论上讲this指针就是函数参数,应该是存放在上的。到底是不是呢,让我们具体来看一看。

VS中查看反汇编

在这里插入图片描述
从上述反汇编可以看出

  • 编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址放在了寄存器ECX中(VC++编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中。而this指针则是存放在寄存器中。
  • 类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量
  • 不能为空。如前面的图编译器处理成员函数隐含的this指针,需要通过this指针取访问对象的成员

Linux中查看反汇编

源代码
#include <iostream>

class Date{
	public:
		void Display(){
			std::cout << _year << "-" << _month
			   	<< "-" << _day << std::endl;
		}
		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, d2;

	d1.Init(1995, 9, 16);
	d2.Init(2019, 5, 7);

	d1.Display();
	d2.Display();

	return 0;
}
反汇编
[sss@aliyun this]$ g++ -S this_storage_location.cc -o this_storage_location.s
main:
.LFB973:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$32, %rsp
	leaq	-16(%rbp), %rax
	movl	$16, %ecx
	movl	$9, %edx
	movl	$1995, %esi
	movq	%rax, %rdi
	call	_ZN4Date4InitEiii
	leaq	-32(%rbp), %rax
	movl	$7, %ecx
	movl	$5, %edx
	movl	$2019, %esi
	movq	%rax, %rdi
	call	_ZN4Date4InitEiii
	leaq	-16(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN4Date7DisplayEv
	leaq	-32(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN4Date7DisplayEv
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE973:
	.size	main, .-main
	.type	_Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:

从上面的反汇编代码我们可以发现,看不懂,O(∩_∩)O哈哈~
所以,g++编译器中this指针存在哪呢?不知道

代码演示

#include <iostream>
using namespace std;

class Sss{
	public:
		void sssDisplay(){
			cout << s << endl;
		}

		void sssTest(){
			cout << "hello, world!" << endl;
		}
	private:
		int s = 0;
};

int main(){
	Sss* p = NULL;

	p->sssDisplay();
	p->sssTest();

	return 0;
}
运行结果
[sss@aliyun this]$ !g++
g++ this_test.cpp -o this_test -std=c++11
[sss@aliyun this]$ ./this_test 
Segmentation fault

段错误,程序到底在哪崩溃了呢?
使用gdb进行调试

[sss@aliyun this]$ ulimit -c 1024
[sss@aliyun this]$ g++ -g this_test.cpp -o this_test -std=c++11
[sss@aliyun this]$ ./this_test 
Segmentation fault (core dumped)
[sss@aliyun this]$ gdb this_test
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-114.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/sss/prictice/C++/this/this_test...done.
(gdb) core-file core.18469 
[New LWP 18469]
Core was generated by `./this_test'.
Program terminated with signal 11, Segmentation fault.
#0  0x000000000040089e in Sss::sssDisplay (this=0x0) at this_test.cpp:7
7                               cout << s << endl;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.3.x86_64 libgcc-4.8.5-36.el7_6.1.x86_64 libstdc++-4.8.5-36.el7_6.1.x86_64

由上述可知,程序崩溃在第7行,对成员变量进行访问的时候,这刚好验证了前面说的this指针不能为空
注意成员函数的声明和定义的参数列表中不能显式写出this对象调用成员函数也不能显式的传该对象自己的地址。但是成员函数体内可以使用this指针

不行:
void Display(&d1);
d1.Display(&d1);

可以:
void Display(){
	cout << this->year << "-" << this->month << "-" << this->day << endl;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值