C++ 程序设计 第3章 类和对象进阶

第3章 类和对象进阶

1 构造函数

作用

对于全局变量,如果程序员在声明变量时没有进行初始化,则系统自动为其初始化为0。

对于局部变量,系统不进行自动初始化,如果程序员没有设定,则是一个随机值

C++提供了一种称为构造函数的机制,用于对对象进行初始化,实际上是用来为成员变量赋初值的。

如果程序员没有编写类的任何构造函数,则由系统自动添加一个不带参数的构造函数。

定义

构造函数的函数名与类名相同,没有返回值。构造函数允许重载

声明格式

class 类名{
	类名(形参1,形参2,形参3,···,形参n);
}

当类中没有定义任何构造函数时,系统会自动添加一个参数表为空,函数体也为空的构造函数,称为默认构造函数。

所以任何类都可以保证至少有一个构造函数。如果已经定义了构造函数,系统则不会再添加默认构造函数。

出于程序安全性考虑,最好为类明确定义一个参数表为空的构造函数,无参构造函数。

类体外定义构造函数时有三种形式

//形式一 冒号后面的内容称为构造函数初始化列表
//每个成员变量后面跟一个放在圆括号中的初始化表达式,这个表达式可以是任何合理的表达式,用来给对应的成员变量赋初值
//最简单的初始化表达式就是形参的值
类名::类名(形参1,形参2,···,形参n):x1(形参1),x2(形参2),···,xn(形参n){}
//形式二
类名::类名(形参1,形参2,···,形参n){
    x1 = 形参1;
    x2 = 形参2;
    ···
    xn = 形参n;
}
//形式三
类名::类名(){
    x1 = 初始化表达式1;
    x2 = 初始化表达式2;
    ···
    xn = 初始化表达式n;
}

三种形式可以混用,构造函数参数的排列顺序可以是任意的,只要保证能与类的成员变量相对应即可。

构造函数用来为对象进行初始化,所以再构造函数中主要的工作是显式的为成员变量赋初值。赋初值的地方有两处,一处是在初始化列表中,另一处是在构造函数体内。类中声明的成员变量至少要在这两个地方之一进行初始化。构造函数中为成员变量初始化时,既不要有重复,也不要有遗漏。

构造函数可以设置默认参数

使用

当程序创建一个对象时,系统自动调用构造函数来初始化对象。构造函数都是自动调用的,对象生成后,对这个对象就不会再执行它的构造函数了。

C++规定,创建类的任何对象时都一定会调用构造函数进行初始化。对象需要占据内存空间,生成对象时,为对象分配的这段内存空间的初始化由构造函数完成。

根据创建对象的语句所提供的参数,系统可以决定调用哪个构造函数。如果没有提供参数,则要调用无参的构造函数。如果调用语句中提供的实参与构造函数中参数列表中形参不匹配,会出现编译错误。

使用构造函数创建对象指针,使用new创建对象,调用无参构造函数,还可以带括号和不带括号。

对有自己有构造函数的类,无论加不加括号,都调用构造函数进行初始化

对使用默认构造函数的类,不加括号时,系统只为成员变量分配内存空间,但不进行内存的初始化,成员变量的值是随机值。而加了括号时,系统在为成员变量分配内存的同时,将其初始化为0

对于基本数据类型的成员变量,如果程序中没有进行显式的初始化,则系统使用0进行初始化。

创建对象指针没有初始化则不会调用构造函数。

复制构造函数

复制构造函数的作用通常时使用已有的对象来建立一个新对象。

使用一个已存在的对象去初始化另一个正在创建的对象,复制构造函数只有一个参数,参数类型是本类的引用。复制构造函数的参数可以是const引用,也可以是非const引用,一个类可以写两个复制构造函数,一个函数的参数是const引用,另一个函数的参数是非const引用

这样,当调用复制构造函数时,既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。

A::A(const A &);
A::A(A &);

如果类中没有给出复制构造函数,那么编译器会自动生成一个复制构造函数。在大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。

编译器自动生成的复制构造函数称为默认复制构造函数。默认复制构造函数不一定存在,但是复制构造函数总是存在的。如果程序中自定义了复制构造函数,则编译器只调用自定义的复制构造函数。

声明和实现复制构造函数的一般格式如下:

class 类名{
public:
	类名(形参表);//构造函数
    类名(类名 & 对象名);//复制构造函数
};
类名::类名(类名 & 对象名){
    函数体
}

自动调用赋值构造函数的情况有3种

当用一个对象去初始化本类的另一个对象时

类名 对象名2(对象名1);
类名 对象名2 = 对象名1;

如果函数F的参数是类A的对象,那么当调用F时,会调用类A的复制构造函数,作为形参的对象,使用复制构造函数初始化的,而且调用复制构造函数时的参数就是调用时所给的实参。

如果函数的返回值是类A的对象,那么当函数返回时,会调用类A的复制构造函数

在复制构造函数的参数表种,加上const是更好的做法,这样复制构造函数才能接收常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。

类型转换构造函数

如果函数只有一个参数,则可以看作是类型转换构造函数,它的作用是进行类型的自动转换。

A a(4);
a = 6;//会将6转换成一个A对象

类型转换构造函数能使一个其他类型的变量或常量自动转换成一个临时对象。

2 析构函数

析构函数的名字也和类名相同,但要在类名前面加一个 ~ 字符,以区别于构造函数。析构函数没有参数,也没用返回值。一个类中有且仅有一个析构函数,如果程序没有定义析构函数,则编译器自动生成默认的析构函数。析构函数不可以多于1个,不会有重载的析构函数。默认析构函数的函数体为空。

在对象消亡时自动调用析构函数,作用是做一些善后处理的工作。如果在创建对象时使用new运算符动态分配了内存空间,则在析构函数中应该使用delete释放掉这部分占用的空间,保证空间可再利用。

先成员对象的构造再对象的构造,然后对象的析构再成员对象的析构

对于不是使用new动态生成的对象,则在对象生存结束时调用析构函数,然后回收这个对象占用的内存。

全局对象和静态对象,则在程序运行结束之前调用其析构函数。

对于对象数组,要为它每个元素调用一次构造函数和析构函数,全局对象数组的析构函数在程序结束之前被调用

创建对象指针数组,消亡时要分别释放空间,析构函数的调用执行顺序与构造函数正好相反

3 类的静态成员

静态变量

根据定义的位置不同,分为静态全局变量和静态局部变量

全局变量是指在所有花括号之外声明的变量,其作用域时全局可见的。使用 static 修饰的全局变量是静态全局变量,其作用域有所限制,仅在定义该变量的源文件有效,项目中的其他源文件中不能使用它。

使用static修饰的局部变量只在局部作用域有效,但却有全局生存期,静态局部变量在程序的整个运行期间都存在,它占据的空间一直到程序结束时才释放,但仅在定义它的块中有效,在块外不能访问它。

静态变量均存储在全局数据区,静态局部变量只执行一次初始化。如果程序未显式给出初始值,则相当于初始化为0,如果显式给出初始值,则在该静态变量所在块第一次执行是完成初始化。

静态局部变量改变值后以后再执行到初始化语句将不会初始化

自动变量在退出函数时消失,下次在执行函数时又会生成新的自动变量,而静态变量在退出函数后并不会消失,所以下次执行函数时会在这个值上进行累加。

类的静态成员

有两种,静态成员变量和静态成员函数,定义成员时在前面添加static,该成员成为静态成员

类的静态成员被类的所有对象共享,不论有多少对象存在,静态成员都只有一份保存在公用内存中。对于静态成员变量,各对象看到的值是一样的。

定义类静态成员变量时,在类定义中声明静态成员变量,然后必须在类体外定义静态成员变量的初值,这个初值不能在类体内赋值。

赋初值格式

类型 类名::静态成员变量 = 初值;

在类体外为静态成员变量赋初值时,前面不能加static 关键字,以免和一般的静态变量相混淆。在类体外定义成员函数时,前面也不能加static关键字。

访问静态成员变量

类名::静态成员变量;
对象名.静态成员变量;
对象指针->静态成员变量;

类的静态成员函数没有this指针,不能在静态成员函数内访问非静态的成员,即通常情况下,类的静态成员函数只能处理类的静态成员变量。静态成员函数内也不能调用非静态成员函数。非静态成员函数可以调用静态成员函数。

设置静态成员的目的是为了将与某些类紧密相关的全局变量和全局函数写到类里面,形式上成为一个整体。

4 变量及对象的生存期和作用域

变量的生存期和作用域

变量的生存期指的是变量所占据的内存空间由分配到释放的时期。变量有效的范围称为其作用域

全局变量及静态变量分配的空间在全局数据区,他们的生存期为整个程序的执行期间。

局部变量,如在函数内或程序块内说明的变量,被分配倒局部数据区,如栈区。生命期从说明处开始到程序块结束处结束

静态变量,没有进行初始化系统会自动初始化为0,局部变量没有进行初始化,则其值是不确定的。

使用new运算符创建的变量具有动态生存期。从生命处开始,直到用delete运算符释放存储空间或程序结束时,变量生存期结束。

每个标识符都在程序的一定范围内有意义,这个范围就是该名字的作用域。这些作用域分为全局域和局部域两类

  1. 全局域包括程序作用域和文件作用域,局部域包括类作用域,函数作用域,块作用域和函数原型作用域

  2. 程序作用域也称为多文件作用域,属于程序作用域的有通过extern关键字进行说明的外部变量以及外部函数。

  3. 文件作用域也成为单文件作用域。定义在所有函数之外的标识符具有文件作用域,作用域为从定义处开始,到整个源文件结束。文件中定义的全局变量和函数都具有文件作用域。如果某个文件中说明了具有文件作用域的标识符,该文件又被另一个文件包含,则该标识符的作用域延申到新的文件中。如 cin cout

  4. 类作用域的有效范围为所定义的那一个类的类体内。类中的私有成员的作用域仅在其类内存,公有成员以及保护成员的作用域有所不同,受到访问控制说明符限制。

  5. 函数作用域的有效范围为所处的那一个函数的函数体内。属于函数作用域的标识符包括函数的形参,在函数体内定义的变量及语句标号

  6. 块作用域是指块中定义的标识符在块中有效。对于块中又嵌套其他块的情况,如果嵌套块中有同名的局部变量,则服从局部优先原则,即在内层块中屏蔽外层块中的同名变量,内层块中局部变量的作用域为内层块,外层块中局部变量的作用域为外层块除去包含同名变量的内层块部分。如果块中定义的局部变量与全局变量同名,块中仍然局部变量优先,在块内可以通过域运算符::访问同名的全局变量。

  7. 函数原型作用域,最小的作用域,范围只在两括号间,所以一般省略形参名

重名标识符的作用域遵循如下的规则

  1. 没有包含关系的两个不同作用域
  2. 具有包含关系的两个不同作用域,将它们看成是互不相同的名字,进入子范围将屏蔽父范围的名字。

类对象的生存期和作用域

在构造函数和析构函数调用之间即是对象的生存期。类中对象有不同的访问权限,还有静态与普通之分,那该怎么判定对象的生存期与作用域呢。

5 常量成员和常引用成员

使用const修饰的成为常量,由关键字const修饰的类成员变量称为类的常量成员变量。必须进行初始化,而且只能通过构造函数的成员初始化列表的方式进行。使用const修饰的函数称为常量函数,定义类的对象时如果在前面添加const关键字,则该对象称为常量对象

定义常量对象或常量成员变量格式

const 数据类型 常量名 = 表达式;

定义常量函数格式

类型说明符 函数名(参数表) const;

在对象被创建后,其常量成员变量的值就不允许被修改,只可以读取其值。对于常量对象,只能调用常量函数。总之,常量成员变量的值不能修改,常量对象中的各属性值均不能修改。

使用常量对象不能调用非常量成员函数,普通成员函数在执行过程中有可能修改对象的值,为了保证常量对象的不变性,不允许通过常量对象调用普通成员函数

通过常量对象仅可以调用常量成员函数,是因为常量成员函数确保不会修改任何非静态成员变量的值。编译器如果发现常量成员函数内出现了有可能修改非静态成员变量的语句,就会报错。

因此,常量成员函数内部也不允许调用同类的其他非常量成员函数(静态成员函数除外)

通常情况下,如果一个成员函数中没有调用非常量成员函数,也没用修改成员变量值,那么将其写成常量成员函数是好的习惯。

6 成员对象和封闭类

一个类的成员变量如果是另一个类的对象,则该成员变量称为成员对象。这两个类为包含关系,包含成员对象的类叫做封闭类

封闭类构造函数的初始化列表

封闭类的成员对象也要在封闭类构造函数中初始化,格式如下

封闭类名::构造函数名(参数表):成员变量1(参数表),成员变量2(参数表),...{...}

初始化列表中的变量既可以是成员对象,也可以是基本数据类型的成员变量。

参数表可以是成员对象的构造函数,也可以是表达式。

生成封闭类对象的语句都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化。

封闭类对象生成时,先执行所有成员对象的构造函数,然后执行封闭类自己的构造函数。成员对象构造函数的执行次序与成员对象在类定义中的说明次序一致,与他们在构造函数初始化列表中出现的次序无关。

当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构。这是C++处理次序问题时遵循的一般规则。

封闭类的对象初始化时要先执行成员对象的构造函数,这是因为封闭类的构造函数中有可能用到成员对象。如果此时成员对象还没有初始化,那就不合理了。

封闭类的复制构造函数

如果封闭类的对象是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化。

7 友元

友元

友元实际上并不是面向对象的特征,而是为了兼顾C语言程序设计的习惯与C++信息隐藏的特点,而特意增加的功能。这是一种类成员的访问权限。友元的概念破坏了类的封装性和信息隐藏,但有助于数据共享,能够提高程序执行的效率。

友元机制是对一些类外的函数打开的一个特殊通道,授权他们能够访问本类的私有成员变量。没有提供授权的函数仍然不能直接访问私有成员变量。这样,既能保证了信息该隐藏的时候隐藏,又能给一些可信任的类和函数提供信息访问的便利性。

友元使用关键字 friend 标识。在类定义中,当friend 出现在函数说明语句的前面时,表示该函数为类的友元函数。一个函数可以同时说明为多个类的友元函数,一个类中也可以有多个友元函数。当friend出现在类名之前时,表示该类为类的友元类。

友元函数

在友元函数内部可以直接访问本类对象的私有成员。不能把其他类的私有成员函数声明为友元函数。友元函数不是类的成员函数,但允许访问类中的所有成员。在函数体中访问对象成员时,必须使用对象名.对象成员名的方式。

友元函数不受类中的访问权限关键字限制,可以把它放在类的公有,私有,保护部分,结果是一样的。

友元函数有两种声明方式,声明全局函数为友元函数,声明成员函数为友元函数

class 类B{
	friend 返回值类型 函数名(参数表);//全局函数
	friend 返回值类型 类A::类A的成员函数名(参数表);//类A的成员函数
}

在主函数中,调用全局友元函数时,直接写函数名即可。但调用类的成员函数时,必须通过类的对象才可实现。因为两个函数都是类B的友元,所以在函数内部均可直接访问类B的成员变量。

友元函数中,参加运算的所有运算分量(如类变量)必须显式的列在友元函数的参数表中,而使用成员函数的方式时,调用对象也是参与运算的分量,当所定义的运算多于一个运算对象时,才将其余运算对象显式的列在该成员函数的参数表中,否则参数表为空

调用友元函数时不通过类对象来调用,而调用成员函数时,需要通过对象来实现。采用成员函数方式,总以当前调用者对象作为该成员函数的隐式第一运算分量。

友元类

如果将一个类B说明另一个类A的友元类,则类B中的所有函数都是类A的友元函数,在类B的所有成员函数中都可以访问类A中的所有成员。在类定义中声明友元类的格式如下

class 类A{}
	friend class 类B;//像属性一样声明在另一个类中
}

友元类的关系是单向的,若说明类B是类A的友元类,不等于类A也是类B的友元类。友元类的关系不能传递,即若类B是类A的友元类,而类C是类B的友元类,不能与类C是类A的友元类。

除非确有必要,一般不把整个类说明为友元类,而仅把类中的某些成员函数说明为友元函数。

8 this指针

C++规定,当调用一个成员函数时,系统自动向它传递一个隐含的参数,该参数是一个指向调用该函数的对象的指针,称为this指针,从而使成员函数知道对哪个对象进行操作。在程序中,可以使用关键字this来引用该指针。this指针是C++实现封装的一种机制,他将对象和该对象调用的成员函数联系在一起,从外部看来,好像每一个对象都拥有自己的成员函数。

类的每个成员函数中都包含this这个特殊的指针,它指向本类对象,值为当前被调用的成员函数所属对象的起始地址。

对于非静态成员函数,系统传递给它的形参个数比程序员写的多一个。多出来的参数就是所谓的this指针,这个this指针指向了成员函数作用的对象。C++规定,在非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象的指针。

在类的成员函数中,当形参的名字与成员变量的名字相同时,往往都要在成员变量名的前面加上this-> ,以区分成员变量名和形参名。在不引起歧义时,可以省略 this-> ,系统采用默认配置。

友元函数不通过对象调用,所以没有this指针。

静态成员函数没有this指针。

*this 返回对象本身。

类中不写返回类型的函数

不写默认返回值为int,也可以不写return int值,在Clion中不能写,直接红线警告,但是在Dev-C++能编译,也能运行。

posted @ 2023-03-20 02:13  快乐在角落里  阅读(86)  评论(0编辑  收藏  举报