文章目录
前言
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
一、类和对象
类是对一组具有相同属性的行为和实体的抽象描述,是一组实体集的概念模型,是面向对象程序设计中的一种构造性的数据类型。其中,相同属性数字化为属性,相同行为数字化为方法。对象是现实世界中对象的数字化抽象,是由描述对象的数据和有关操作组成的封装体与客观实体有直接的对应关系。
类是抽象的,是对象的模板,不占用存储空间。而对象是具体的,是类的实例化,占用存储空间类和对象密切相关,是面向对象编程技术中最基本的概念
1.类与对象的思想
面向对象的特点:抽象、封装、继承、多态
面向对象编程的特点:
- 易维护:可读性高,即使改变了需求,由于继承的存在,只需要对局部模块进行修改,维护起来非常方便,维护的成本也比较低。
- 质量高:可以重用以前项目中已经被测试过的类,使系统满足业务需求从而具有更高的质量
- 效率高:在软件开发时,根据设计的需求要对现实世界的事物进行抽象,从而产生了类
- 易扩展:由于继承、封装、多态的特性,可设计出高内聚、低耦合的系统结构,使系统更加灵活、更容易扩展,而且成本也比较低。
2.类定义的格式
类是一种复杂的数据类型,它是将不同类型的数据和这些数据相关的操作封装在一起的集合体。类中的成员有数据成员和函数成员两种。
- 数据成员是用来描述对象属性的静态成员。
- 函数成员是用来描述对象属性的动态成员。
类定义的格式如下:
class <类名>
{
public:
<数据成员或函数成员的说明>
protected:
<数据成员或函数成员的说明>
private:
<数据成员或函数成员的说明>
};
其中class是定义类的关键字,<类名>是一种标识符类名的首字符,通常大写。花括号内是类的说明部分,用来说明该类的成员。
从访问权限上来,分类的成员分为公有的(public),私有的(private)和保护的(protected) 3类。关键字public 、private 、protect,被称为访问权限修饰符或访问控制修饰符,公有成员用public来说明,公有成员可以在任意函数中访问私有成员;用private来说明,私有成员只能在本类的成员函数中访问;保护成员用protected说明,保护成员可以在本类及其派生类的成员函数中访问。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Person
{
public:
void show()
{
cout<<_name<<" "<<_sex<<" "<<_age<<endl;
}
private:
char* _name;
char* _sex;
int _age;
};
注意:这里Person类的成员函数show将会作为内联函数处理
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
class Person
{
public:
void show();
private:
char* _name;
char* _sex;
int _age;
};
void Person::show()
{
cout<<_name<<" "<<_sex<<" "<<_age<<endl;
}
3.类的访问限定符及封装
C++封装:在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
4.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void show();
private:
char* _name;
char* _sex;
int _age;
};
void Person::show()
{
cout<<_name<<" "<<_sex<<" "<<_age<<endl;
}
指明 show 函数属于 Person
5.类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个
类,来描述具体学生信息。 - 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。 - 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设
计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象
才能实际存储数据,占用物理空间。
class Person
{
public:
void show();
private:
char* _name;
char* _sex;
int _age;
};
void Person::show()
{
cout<<_name<<" "<<_sex<<" "<<_age<<endl;
}
int main()
{
Person A; //对象的实例化
}
创建 Person 类对象 A 的过程,就称为对象的实例化
这里提一句 :用户存储空间分为程序区(代码区)、静态存储区(数据区)、和动态存储区(栈区和堆区)。代码区存放程序代码。程序运行前就分配存储空间。数据区存放常量静态变量(对象),全局变量(对象)等。程序运行前就分配存储空间一直保留到程序结束,栈区存放局部变量(对象),函数参数,函数返回值和临时变量(对象)等。编译时程序在栈区中留出一定的空间。程序运行时按先进后出原则进入栈区。
6.类对象模型
计算类对象的大小
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?
首先我们对类对象的存储方式大胆猜测:对象中包含类的各个成员。
但是仔细一想就会发现问题,如果每个对象都包含类的各个成员,那么假设一个类有20个成员函数,按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,大大浪费了空间。这样肯定不是高效的。
所以实际情况是:
- 代码只保存一份,在对象中保存存放代码的地址
- 只保存成员变量,成员函数存放在公共的代码段
7.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指针,有以下几点总结:
- this指针指向当前对象,可以访问当前对象的所有成员变量。包括private、protected、public。
- this指针是const指针,一切企图修改该指针的操作,如赋值(改变指向)、增减都是不允许的!
- this指针只有在成员函数中才有定义。因此,在创建一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。
- 只有创建对象后,this指针才有意义。
- static静态成员函数不能使用this指针。原因静态成员函数属于类,而不属于某个对象,所以static静态成员函数压根就没有this指针。
- this在成员函数的开始执行前构造的,在成员函数的执行结束后清除。至于如何清除的,由编译器实现,程序员不关心。this是通过函数参数的首参数来传递的。
二. 类的6个默认成员函数
C++共有以下6个默认成员函数:
构造函数、析构函数、拷贝构造函数、赋值运算符重载、const成员函数、取地址及const取地址操作符重载
这些默认成员函数的特点是如果程序员没有自己编写,那么系统会自动生成,但如果编写了默认的就不再生成
1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
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;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
通过Date today定义日期对象,然后通过today.Init为该日期对象的,成员赋初值。这种情况是先定一个对象,然后再对其付初始值,这种模式显然不符合客观规律,因为当一个对象已经客观存在,他就应该具备相关的属性值,而不应该再为其赋值,那么能否有一种机制在一个类对象定义的同时,为其数据成员自动付相应的初值呢?
为了做到这一点,C++引入了构造函数的概念。
构造函数是与所在类同名的成员函数。在创建类对象的过程中,由系统自动调用,因此可以将创建的对象的初始化工作放在构造函数中完成。C++引入构造函数的本意就在于此,在使用时注意构造函数不允许向其他函数那样由用户直接调用(显示调用),因为只要调用一次构造函数,就立即创建一个新对象。以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
构造函数的特点如下:
- 构造函数是与所在类同名的成员函数。
- 构造函数无返回值。
- 构造函数可以重载。
- 构造函数的在创建类时自动调用,不允许像其他函数那样由用户调用。
- 系统默认的构造函数为 X(){},即空构造函数。
- 只要定义了一个构造函数,系统默认的构造函数(空构造)就不存在。
2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数在一个对象退出生存期时,被系统自动调用,因此可以将对象消亡前需要做的一些工作定义在析构函数中。在析构函数中可以释放该对象占用的一些资源,比如说该对象有指针成员,该指针成员在程序运行过程中申请的动态空间,这些动态空间在对象消亡前要进行释放,这样操作就可以在析构函数中完成。如果在类中没有定义虚构函数,则系统自动为类提供一个默认析构函数,这个析构函数的函数体是空的,什么也不做。
class Test
{
public:
Test()
{
arr=(int*)malloc(sizeof(int)*10);
}
~Test()
{
free(arr);
arr=NULL;
}
private:
int* arr;
};
int main()
{
Test A;
}
在退出 main 函数时系统自动调用析构函数,释放掉动态分配的数组,并置空;
析构函数不返回任何值,没有返回类型,也没有函数参数。由于没有函数参数,因此它不能被重载。换言之,一个类可以有多个构造函数,但是只能有一个析构函数。
析构函数的特点如下:
- 函数名同类名,函数名前加~。
- 析构函数无返回类型,无参数,不能重载,访问权限一般为public。
- 析构函数在对象消亡前自动调用。
- 构造函数的在创建类时自动调用,不允许像其他函数那样由用户调用。
- 系统默认的析构函数为 ~X(){}
- 析构函数调用次序与构造函数调用次序相反,即后构造的对象先析构。
3.拷贝构造函数
拷贝构造:拷贝构造函数,又称构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构造及初始化。
class Array
{
int *a;
int n;
public:
Array(int aa[],int nn) //构造函数
{
n=nn;
a=(int*)new int[n];
for(int i=0;i<n;i++)
{
a[i]=aa[i];
}
}
~Array() //析构函数
{
delete[] a;
}
void print()
{
for(int i=0;i<n;i++)
{
cout<<a[i]<<" ";
}
cout>>endl;
}
}
int main()
{
int aa[6]={1,2,3,4,5,6};
Array a1(aa,6);
Array b1(a1);
a1.print();
b1.print();
}
上述程序中定义了两个数组类 a1 和 a2 ; 对象 b1 是通过 a1 来创建的,创建 b1 时调用默认的赋值构造函数,该默认的赋值构造函数如下。
Array(Array &src)
{
(*this).a = src.a;
(*this).n = src.n;
}
这样使得对象 a1 和 b1 的指针成员 a 都指向同一空间,内存示意图如下:
默认的复制构造函数只将指针指向原来的数组,称为“浅复制”。以此达到复制的效果,实际上 a1 和 b1 的数组都共用同一块空间,那么当程序运行结束的时候,b1 先调用析构函数,将数组所占用的空间释放掉,当 a1 调用析构函数时,将会再一次对 a 所指向的数组空间进行释放,然而数组已经被 b1 的析构函数释放过,再次释放将会造成同一块空间释放两次,从而导致程序出错 。
所以为了解决这个问题,需要我们自己写一个拷贝构造函数,为 b1 的 a 声请一块新的内存空间,同时吧 a1 的数组内容拷贝过来 ,以此达到真正意义上的复制。称为“深复制”。具体代码实现如下:
Array::Array(Array & aa)
{
n=aa.n;
a=new int[n];
for(int i=0;i<n;i++)
{
a[i]=aa.a[i];
}
}
这样 a1 b1 的都有属于自己的数据成员,没有共用同一块空间的情况,在析构时也就不会出错了。
注意:这里必须用引用的方式,若只是按值传递,在实参象形参传递时,需要复制一份数据(函数传参时将实参拷贝一份给形参的),但因为没有拷贝构造函数(系统自带的无法使用,因为你定义了拷贝构造,默认的就不会出现了),所以不可能完成复制,则会循环嵌套调用你定义的非引用方式的拷贝构造函数。因此,在这里必须使用引用的方式。
未完待续…