三、类和对象进阶

构造函数

变量 初始化
全局变量 如果程序员在声明变量时没有进行初始化,
则系统自动为其初始化为0。
这个工作在程序启动时完成。
局部变量 系统不进行自动初始化,
所以它的处置需要靠程序员给定。
如果程序员没有设定,
则是一个随机值。
  • 为了对对象进行初始化,c++提供了一种称为构造函数的机制,用于对对象进行初始化,实际上是用来为成员变量赋初值的。
  • 构造函数是类中的特殊成员函数,它属于类的一部分。给出类定义时,由程序员编写构造函数。如果程序员没有编写类的任何构造函数,则由系统自动添加一个不带参数的构造函数。
  • 声明对象后,可以使用new运算符为对象进行初始化,此时调用的是对象所属类的构造函数。构造函数的作用是完成对象的初始化工作,用来保证对象的初始状态是确定的。在对象生成时,系统自动调用构造函数,用户在程序中不会直接调用构造函数。

构造函数的定义

定义一个类时,需要为类定义相应的构造函数。构造函数的函数名与类名相同,没有****返回值****。一个类的构造函数可以有多个,即****构造函数允许重载。同一个类的多个构造函数的参数表一定不能完全相同。

构造函数的声明格式如下:

类名(形参1, 形参2, ..., 形参n);
  • 在声明类的构造函数时可以同时给出函数体,这样的构造函数称为内联函数。也可以在类体外给出构造函数的定义。构造函数的声明中,形参的个数可以为0,即参数表为空。
  • 当类中没有定义任何构造函数时,系统会自动添加一个参数表为空、函数体也为空的构造函数,称为默认构造函数。所以任何类都可以保证至少有一个构造函数。
  • 如果程序员在程序中已经定义了构造函数,则系统不会再添加默认构造函数。

假设类的成员变量有三个,则在类体外定义构造函数时通常有如下3中形式:

//方式一
//使用带入的参数值通过初始化列表为各成员变量赋初值
Student::Student(int sno, int age, string name):_sno(sno), _age(age), _name(name) {
    
}
//或者
//使用固定值在初始化列表中为个成员变量赋初值
Student::Student():_sno(11111), _age(13), _name("slh") {

}

//方式二
Student::Student(int sno, int age, string name) {
    _sno = sno;
    _age = age;
    _name = name;
}

//方式三
Student::Student() {
    _sno = 12345;
    _age = 12;
    _name = "tyg";
}

再比如类Student已经声明了下列4个构造函数:

class Student {
public:
    Student();
    Student(int);
    Student(int, int);
    Student(int, int, string);
    void printStudent();
private:
    int _sno;
    int _age;
    string _name;
};

在类体外定义构造函数

Student::Student():_sno(12321), _age(23), _name("df") {
    
}
Student::Student(int sno):_age(12), _name("dfs") {
    _sno = sno;
}
Student::Student(int sno, int age):_name("dsf") {
    _sno = sno;
    _age = age;
}
Student::Student(int sno, int age, string name) {
    _sno = sno;
    _age = age;
    _name = name;
}

构造函数的使用

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

特别地,如果程序中声明了对象数组,即数组的每个元素都是一个对象,则一定要为对象所属的这个类定义一个无参的构造函数。因为数组中每个元素都需要调用无参的构造函数进行初始化,所以必须要有一个不带参数的构造函数。

复制构造函数与类型转换构造函数

复制构造函数是构造函数的一种,也称为拷贝构造函数它的作用是使用一个已存在的对象去初始化另一个正在创建的对象。例如,类对象间的赋值是由复制构造函数实现的。

复制构造函数只有一个参数参数类型是****本类的引用复制构造函数的参数可以是const引用,也可以是非const引用。一个类中可以写两个复制构造函数,一个函数的参数是const引用,另一个函数的参数是非const引用。这样,当调用复制构造函数时,既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。对于类A而言,复制构造函数的原型如下:

//格式一
A::A(const A &)
//格式二
A::A(A &)

例如:

//复制构造函数
Student::Student(Student &s) {
    _sno = s._sno;
    _age = s._age;
    _name = s._name;
}
Student::Student(const Student &s) {
    _sno = s._sno;
    _age = s._age;
    _name = s._name;
}

自动调用复制构造函数的情况有以下3种:

  1. 当用一个对象去初始化本类的另一个对象时,会调用复制构造函数。例如,使用下列形式的说明语句时,即会调用复制构造函数。
    1. 类名 对象名2(对象名1);
    2. 类名 对象名2 = 对象名1;
  2. 如果函数F的参数是类A的对象,那么当调用F时,会调用类A的复制构造函数。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
  3. 如果函数的返回值是类A的对象,那么当函数返回时,会掉用类A的复制构造函数。也就是说,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是return语句所返回的对象。

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

析构函数

  • 与构造函数一样,析构函数也是成员函数的一种,它的名字也与类名相同,但要在类名前面加一个~字符,以区别于构造函数。析构函数没有参数,也没有返回值。一个类中有且仅有一个析构函数,如果成员中没有定义析构函数,则编译器自动生成默认的析构函数。析构函数不可以多于一个,不会有重载的析构函数。默认析构函数的函数体为空。
  • 创建对象时自动调用构造函数,那么,什么时候调用析构函数呢?可想而知,在对象消亡时自动调用析构函数。析构函数的作用是做一些善后处理的工作。例如,如果在创建对象时使用new运算符动态分配了内存空间,则在析构函数中应该使用delete释放掉这部分占用的空间,保证空间可再利用。
  • 当使用new运算符生成对象指针时,自动调用本类的构造函数。使用delete删除这个对象时,首先为这个动态对象调用本类的析构函数,然后再释放这个动态对象占用的内存。
Student::~Student() {
    cout << "student对象被释放了" << endl;
}

类的静态成员

  • 与C语言一样,可以使用static说明自动变量。根据定义的位置不同,分为静态全局变量和静态局部变量

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

  • 块内定义的变量是局部变量,从定义之处开始到本块结束处为止是局部变量的作用域。使用static修饰的局部变量是静态局部变量,即定义在块中的静态变量。静态局部变量具有局部作用域,但却具有全局生存期。

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

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

  • 类的静态成员有两种:静态成员变量和静态成员函数。在类体内定义类的成员时,在前面添加static关键字后,该成员即成为静态成员。

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

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

  • 给静态成员变量赋初值的格式如下:

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

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

访问静态成员时,成员前面既可以用类名作前缀,也可以使用对象名或对象指针作前缀。这与访问类成员时仅能使用对象名或对象指针作前缀是不同的。

//访问类静态成员的一般格式如下:
类名::静态成员名
//或者
对象名.静态成员名
//或者
对象指针->静态成员名

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

  • 对于普通成员变量,每个对象有各自的一份,而****静态成员变量只有一份,被同类所有对象共享。
  • 普通成员函数一定是作用在某个对象上的,而静态成员函数并不具体作用在某个对象上。
  • 访问普通成员时,要通过对象名.成员名等方式,指明要访问的成员变量是属于哪个对象的,或要调用的成员函数作用于哪个对象;
  • 访问静态成员时,则可以通过类名::成员名的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。因此,甚至可以在还没有任何对象生成时就访问一个类的静态成员。
  • 非静态成员的访问方式其实也适用于静态成员,也就是可以通过对象名.成员名的方式访问,效果和类名::成员名这种访问方式没有区别。

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

变量的生存期和作用域

  • 变量的生存期是指变量所占据的内存空间由分配到释放的时期。变量有效的范围称为其作用域。全局变量是程序中定义在所有函数(包括main函数)之外的任何变量,其作用域是程序从变量定义到整个程序结束的部分。这意味着全局变量可以被所有定义在全局变量之后的函数访问。全局变量及静态变量分配的空间在全局数据区,它们的生存期为整个程序的执行期间。
  • 而局部变量,如在函数内或程序块内说明的变量,被分配到局部数据区,如栈区等。这种分配是临时的,一旦该函数体或程序块运行结束,所分配的空间就会被撤销。局部变量的生存期从被说明处开始,到所在程序块结束处结束。
  • 对于静态变量,如果没有进行初始化,系统会自动初始化为0。局部变量如果没有进行初始化,则其值是不确定的。
  • 使用new运算符创建的变量具有动态生存期。从声明处开始,直到用delete运算符释放存储空间或程序结束时,变量生存期结束。

类对象的生存期和作用域

类的对象在生成时调用构造函数,在消亡时调用析构函数,在这两个函数调用之间即是对象的生存期。

常量成员和常引用成员

  • 在类中,也可以使用const关键字定义成员变量和成员函数,甚至是类的对象。由关键字const修饰的类成员变量称为类的常量成员变量。类的常量成员变量必须进行初始化,而且只能通过构造函数的成员初始化列表的方式进行。使用const修饰的函数称为常量函数。定义类的对象时如果在前面添加const关键字,则该对象称为常量对象。定义常量对象或常量成员变量的一般格式如下:

    const 数据类型 常量名 = 表达式;
    
  • 定义常量函数的格式如下:

    类型说明符 函数名(参数表) const;
    
  • 在对象被创建以后,其常量成员变量的值就不允许被修改,只可以读取其值。对于常量对象,只能调用常量函数。总之,常量成员变量的值不能修改,常量对象中的各个属性值均不能修改。

例如:

class CDemo {
public:
    void setValue(){};//非常量成员函数
}

int main() {
    const CDemo obj;//obj是常量对象
    obj.setValue();//❌
    
    return 0;
}

成员对象和封闭类

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

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

当生成封闭类的对象并进行初始化时,它包含的成员对象也需要被初始化,需要调用成员对象的构造函数。在定义封闭类的构造函数时,需要添加初始化列表,指明要调用成员对象的哪个构造函数。在封闭类构造函数中添加初始化列表的格式如下:

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

初始化列表中的成员变量既可以是成员对象,也可以是基本数据类型的成员变量。对于成员对象,初始化列表的参数表中列出的是成员对象构造函数的参数(它指明了该成员对象如何初始化)

先调用成员对象的构造函数,再调用封闭类对象的构造函数。

封闭类的复制构造函数

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

友元

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

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

友元函数

在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为友元,这样那些函数就称为本类的友元函数。在友元函数内部可以直接访问本类对象的私有成员。在类定义中,将一个全局函数声明为本类友元函数的格式如下:

friend 返回值类型 类名::类的成员函数名(参数表);

不能把其他类的私有成员函数声明为友元函数。

友元函数不是类的成员函数但允许访问类中的所有成员。在函数体中访问对象成员时,必须使用对象名.对象成员名的方式。

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

//Test.hpp
#include <stdio.h>

class Pixel;

class Test {
public:
    void printP(Pixel p);
    void printP(Pixel *p);
};
//Test.cpp
#include "Test.hpp"
#include "Pixel.hpp"

using namespace std;

void Test::printP(Pixel p) {
    cout << "test:x = " << p.x << ", y = " << p.y << endl;
}
void Test::printP(Pixel *p) {
    //❌写法
    //'x' is a private member of 'Pixel'
    //'y' is a private member of 'Pixel'
    cout << "x = " << p->x << ", y = " << p->y << endl;
}
//Pixel.hpp
#include <stdio.h>
#include <iostream>
#include "Test.hpp"

using namespace std;

class Pixel {
private:
    int x, y;
public:
    Pixel(int x0, int y0) {
        x = x0;
        y = y0;
    }
    void printXY() {
        cout << "x = " << x << ", y = " << y << endl;
    }
    friend double getDist(Pixel p1, Pixel p2);
    friend void Test::printP(Pixel p);
};
//main.cpp
#include <iostream>
#include <cmath>
#include "Test.hpp"
#include "Pixel.hpp"

using namespace std;

double getDist(Pixel p1, Pixel p2) {
    double xDist = double(p1.x - p2.x);
    double yDist = double(p1.y - p2.y);
    return sqrt(xDist * xDist + yDist * yDist);
}

int main(int argc, const char * argv[]) {
    
    Pixel p1(0, 0), p2(10, 10);
    p1.printXY();//x = 0, y = 0
    p2.printXY();//x = 10, y = 10
    cout << "p1和p2的间距 = " << getDist(p1, p2) << endl;
    //p1和p2的间距 = 14.1421
    
    Test t;
    cout << "从友元函数中输出:" << endl;
    t.printP(p1);//test:x = 0, y = 0
    t.printP(p2);//test:x = 10, y = 10
    
    return 0;
}

友元类

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

friend class 类名;

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

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

this指针

  • C++语言规定,当调用一个成员函数时,系统自动向它传递一个隐含的参数。该参数是一个指向调用该函数的对象的指针,称为this指针,从而使成员函数知道对哪个对象进行操作。
  • C++规定,在非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象的指针。
  • 在一般情况下,在不引起歧义时,可以省略this->,系统采用默认设置。
  • 静态成员是类具有的属性,不是对象的特征,this表示的是隐藏的对象的指针,所以静态成员函数没有this指针
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335