IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++类与对象再探 -> 正文阅读

[C++知识库]C++类与对象再探

关键字:const成员函数,构造函数,析构函数,拷贝构造函数,赋值操作符重载,取地址重载,const取地址重载。

目录

编译器如何编译类

const成员函数

特性

?几个小问题

6个默认成员函数

构造函数

概念

特性

编译器生成的默认构造函数

析构函数

概念

特征

编译器自动生成的析构函数

拷贝构造函数

概念

特性

赋值运算符重载

运算符重载

赋值运算符重载

?进一步了解赋值运算符重载

?取地址运算符重载&&const取地址运算符重载


编译器如何编译类

与我们所熟知的编译过程不同,编译器对于类的编译是有选择性,有顺序的。

1.寻找成员变量

? ? ? ? 编译器在编译类的时候并不是与编译其它东西一样,按照在代码中的先后顺序进行的编译。编译器会优先寻找类中的成员变量对其编译。

2.识别成员函数

? ? ? ? 在将成员变量完成编译后,编译器紧接着会识别类中的成员函数。如果是非静态成员函数,编译器会对其进行一些简单的修改——在参数列表中加入this指针。

3.改写成员函数

const成员函数

在C语言中有一个神奇的关键字,const关键字。

它用以给其所修饰的变量赋以常性,即:

const修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的。(C)

关于const的各种各样的用法在这里就不细讲了,有时间的话单独总结一篇博客(flag + 1)。

这里我们只讨论const修饰类的成员函数。

如上所言,const最重要的特征在于——常性。我们可以简单地理解为与常量相似的特性,不可改变

我们将const修饰的类的非静态成员函数称之为const成员函数。

const修饰类的成员函数,它实际上修饰的是非静态成员函数中隐含的this指针,表明该成员函数不能对类的任何成员进行修改。

class date {
public:
    void test() const;
}

//也可以写做

class date {
public:
    void const test();
}

//两者的本质都是void test(const date* this);

特性

1.在C++中只有被声明为const的成员函数才可以被声明为const的对象调用。

class date {
public:
    void test() const{
        cout << "const" << endl;
    }
    void test2() {
        cout << "非const" << endl;
    }
};

int main() {
    const date d;
    d.test();
    d.test2();
    return 0;
}

?通过对代码的编译,我们可以得到如下报错:

?为什么不可以执行这样的转换?我们可以从权限的角度去理解。

const date 是一个常对象,它在被创建出来后,便只具有的权限。

date& 则不一样,它可以对变量进行修改,是具有读与写两种权限的。

具有读写权限的可以操作只需要读权限的,只有读权限的无法操作需要读写两种权限的。

这叫做前置科技不满足。

2.const成员函数可以被拥有相同参数列表的非const成员函数重载。(这算是个伪特性)

?这仔细想想实际上是个伪命题,因为const成员函数的隐含参数this的类型是const 类类型指针,而非const成员函数的隐含参数this的类型是类类型指针。

但从这一点看两者的参数列表间永远差着一个const。

但那时隐含的,我们只关心我们在括号中看到的参数列表,所以,得出了const成员函数可以被拥有相同参数列表的非const成员函数重载这样的结论。

只是看起来如此罢了。

class date {
public:
    void test() const{
        cout << "const" << endl;
    }
    void test() {
        cout << "非const" << endl;
    }
};

int main() {
    const date cd;
    date d;
    cd.test();
    d.test();
    return 0;
}

?几个小问题

根据上文的权限的说法,很容易就可以判断出下面几个小问题的答案。

1.const对象可以调用非const成员函数吗?

读 操作 读写 不可以

2.非const对象可以调用const成员函数吗??

读写 操作 读 可以

3.const成员函数内可以调用其它的非const成员函数吗??

读 操作 读写 不可以

4. 非const成员函数内可以调用其它的const成员函数吗?

读写 操作 读 可以

6个默认成员函数

所谓默认成员函数,是在用户不定义的情况下,编译器会按照其规则自动生成,而如果用户进行显式了定义,编译器便不会再生成的成员函数。

构造函数

概念

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

特性

与我们所熟知的构造的含义不同,构造函数的主要任务并不是开辟空间创建对象,而是初始化对象

构造函数的特征有以下几点:

1.函数名与类名相同

2.没有返回值,也不用声明返回值类型

3.对象实例化时自动调用对应的构造函数

4.构造函数可以重载?

class date {
private:
    int _year;
    int _month;
    int _day;
public:
    date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        cout << "三参数" << endl;
    }

    date(int year) {
        _year = year;
        cout << "一参数" << endl;
    }
};
int main() {
    date d;
    return 0;
}

在以上情况下,编译器是会进行报错的:

可见,当我们显示定义构造函数之后,编译器便不会再生成构造函数。

int main() {
    date d(2001);
    date d2(2001, 1, 12);
    return 0;
}

分别根据两个构造函数,创建两个对象,可以看到程序成功运行:

所以,构造函数可以重载。

5.如果类中没有显式定义构造函数,则编译器会自动生成一个默认无参构造函数,而一旦用户显式定义,编译器将不再生成。?

6.无参构造函数与全缺省的构造函数都称为默认构造函数。默认构造函数只能有一个。

由这一点可以看出,默认构造函数是为了我们可以在如:

date d;

这样的情况下不会报错而设置的。

默认构造函数为什么只能有一个?这其实是很容易理解的。

默认构造函数是需要可以无参调用的。

全缺省的函数调用时是可以不输入任何参数的,而无参函数也是不输入任何参数就可以调用的。同一个类的构造函数,其必然遵循构造函数的原则——函数名为类名。

同名,构成重载,都可以不传参调用。同时存在的情况下必然会导致二义性。

一般情况下,在显式定义构造函数的时候,最好定义一个全缺省的默认构造函数。

编译器生成的默认构造函数

在我们不显式实现构造函数的时候,我们创建一个对象,根据上边说到的特性,我们知道,对象此时会调用编译器自动生成的无参构造函数,但当我们直接输出对象的值的时候,会发现此时对象中成员变量的值依旧是随机值,那么这个构造函数都做了什么?

class Time {
public:
    Time() {
        cout << "time" << endl;
    }
};

class date {
private:
    int _year;
    int _month;
    int _day;
    Time _t;
public:
    void print() {
        cout << _year << _month << _day << endl;
    }
};

int main() {
    date d;
    return 0;
}

看这样一段代码,运行的结果:

我们在创建date类类型的对象时,会创建一个Time类类型的对象作为成员变量。通过运行结果可以看出Time类的构造函数被调用了。但我们创建的明明是date类的对象,那么Time类的构造函数是谁调用的?

答案是显而易见的,我们说过,构造函数是用来对对象进行初始化的,而编译器对于int等内置类型的初始化都是非常简单的放一个随机值——并不是我们不直接写明什么int i = 多少编译器就不给变量初始化了,初始化是分有默认初始化与值初始化两种的,当我们不指定初值的时候,编译器会对内置变量进行默认初始化的。

默认初始化:如果定义变量时没有指定初值,则变量被默认初始化。其初始值和变量的类型以及变量定义的位置相关。默认初始化类对象和默认初始化内置类型变量有所不同。

而对于如类一般的自定义类型的变量,编译器往往会调用它自身的默认构造函数。

?注意,这是编译器自动产生的默认构造函数的一部分,我们从中很容易找到在这之中调用了Time类的构造函数。

简单地总结一下:

C++中的数据类型有内置类型与自定义类型两大类,其中内置类型就是C语言语法已经定义好的类型:如int/char/double...,而自定义类型就是我们使用的诸如:class/struct/union...编译器默认生成的构造函数会自动调用其成员变量中自定义类型的成员变量的默认构造函数。

析构函数

概念

构造函数与创建对象并没有多大关系,同理,析构函数与销毁对象也没有多大关系。

析构函数与构造函数的功能相反,析构函数不是完成对象的销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

这里所谓的资源清理工作,?通常指的是:堆上申请的空间,文件指针,套接字...

这些资源是从计算机中申请的,需要用户手动释放的。如果申请而不释放,则会导致资源的浪费,轻则造成计算机卡顿,重则程序崩溃。

如果我们在类的构造函数中申请了这些资源,每次在对象销毁前都手动将这些资源释放显然不是一个聪明的办法,所以C++引入了析构函数的操作,当对象销毁时,编译器都会自动调用析构函数执行其中相关的清理资源的代码(当然要用户自己写,默认生成的你敢信?)。

特征

析构函数的特征有以下几点:

1.析构函数名为类名前加上~

2.无显式参数,无返回值,也不需要声明返回值类型

3.一个类有且只有一个析构函数。若未显式定义,系统自动生成默认的析构函数。

4.对象生命周期结束时,C++编译器自动调用析构函数。

class date {
private:
    int _year;
    int _month;
    int _day;
public:
    date() {
        _year = 2001;
        _month = 1;
        _day = 12;
        cout << "create" << endl;
    }

    void print() {
        cout << _year << _month << _day << endl;
    }

    ~date() {
        cout << "destory" << endl;
    }
};

int main() {
    do {
        date d;
    } while (0);
    return 0;
}

?

?可以看到,在do...while()循环体中创建了一个date类类型对象,同时调用了date类的构造函数,当循环结束时,编译器自动调用了析构函数~date()

析构函数的存在极大地提高了C++代码的容错率,减少了一些由于写代码时马虎不注意导致的资源浪费。

编译器自动生成的析构函数

class Time {
public:
    Time() {
        cout << "time" << endl;
    }

    ~Time() {
        cout << "endtime" << endl;
    }
};

class date {
private:
    int _year;
    int _month;
    int _day;
    Time _t;
public:
    date() {
        _year = 2001;
        _month = 1;
        _day = 12;
        cout << "create" << endl;
    }

    void print() {
        cout << _year << _month << _day << endl;
    }

    /*~date() {
        cout << "destory" << endl;
    }*/
};

int main() {
    do {
        date d;
    } while (0);
    return 0;
}

与编译器生成的默认构造函数类似,编译器生成的默认析构函数可以调用其成员变量中自定义类型对象对应的析构函数。但如果其成员变量都是内置类型的变量,那么编译器生成的默认析构函数就没有太大的用途了。

注意:编译器自动生成的析构函数不会释放我们在类中申请的资源,如果类涉及到了资源的申请,用户一定要显式定义实现析构函数。

拷贝构造函数

概念

拷贝构造函数只有一个形参,该形参是对本类类型对象的引用。拷贝构造函数在使用已存在类类型对象创建新对象时由编译器自动调用。

特性

拷贝构造函数有以下特征:

1.拷贝构造函数是构造函数的重载

2.拷贝构造函数的参数只有一个且必须用引用传参,使用传值会引发无穷递归调用。

这个原理是很简单的,传值传参,传进去的形参是实参的拷贝。?

实现对象拷贝功能的函数正是我们这里正在讲的拷贝构造函数……然后,就递归了。

http://t.csdn.cn/skCxQ

我之前针对拷贝构造函数专门写过一篇博客,有兴趣的朋友可以看一下。

3.如果没有显式定义拷贝构造函数,系统生成默认的拷贝构造函数。默认的拷贝构造函数只是对对象按内存存储按字节序完成拷贝,这种拷贝方式被称为浅拷贝。

赋值运算符重载

运算符重载

为了增强代码的可读性,C++引入了运算符重载,赋予运算符新的功能。

运算符重载是具有特殊函数名的函数,它也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表和普通的函数类似。

//函数名字为,关键字operator后面跟需要重载的运算符符号

返回值类型 operator操作符(参数列表);

?注意:

1.不能连接非操作符来创建新的操作符,如operator@

2.重载操作符必须有一个类类型或者枚举类型的操作数。

3.用于内置类型的操作符,其含义不能改变

4.作为类的重载函数时,形参看起来比操作数目少1,这是因为有隐式形参this,限定为第一个形参。

5.以下5个运算符不能重载:

成员指针访问运算符域运算符长度运算符条件运算符成员访问运算符
.*(->*)::sizeof?:.

赋值运算符重载

所谓赋值运算符,就是我们都熟知的 “ = ”运算符。我们可以为其简单定义一些功能:

class date {
private:
    int _year;
    int _month;
    int _day;
public:
    date(int year = 1999, int month = 2, int day = 12) {
        _year = year;
        _month = month;
        _day = day;
    }

    void print() {
        cout << _year << _month << _day << endl;
    }

    date& operator=(date& d) {
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        cout << "operator=" << endl;
        return *this;
    }

    ~date() {
        cout << "destory" << endl;
    }
};

int main() {
    date d1(2002, 2, 2);
    date d2;
    d2.print();
    d1.print();
    d2 = d1;
    d2.print();
    return 0;
}

这里需要再重复一下我们上方运算符重载的注意事项:尽可能地保证每个运算符重载后的功能与其对内置类型变量的操作一致。

虽然有些更改操作不会对程序运行造成什么影响,但是标新立异的后果是程序的可维护性与可读性都将大幅度降低,得不偿失。

如果用户没有显式定义赋值运算符重载,编译器会自己生成一个,完成对象的按字节序的值拷贝,也即浅拷贝。

与构造函数与析构函数相同,编译器默认生成的赋值运算符并不包含对于需要用户手动申请释放的资源的操作。

编译器自动生成的赋值运算符重载只是简单的讲这片内存的值拷贝到了那片内存,比如对象1在堆上申请的空间,如果我们没有显式定义赋值运算符重载,当我们将1的值赋给2的时候,编译器会调用其自己自动生成的赋值运算符重载的功能,而它所做的,仅仅是将1的栈帧上的变量的值拷贝到了2的栈帧上——这意味着它只是将指向堆上那片空间的指针的值拷贝了过来。对象1与对象2所操作的实际上是堆上的同一块空间。

?且不说这样的情况下会对代码中的逻辑计算结果有什么影响,从其它角度来看,可能会导致的后果有:

1.由浅拷贝导致的,对象1与对象2中某个成员变量指向同一块内存,以至于在对象1与对象2析构是,一块内存释放两次导致程序崩溃。

2.复制成功后,对象2中原本申请的空间权柄丢失,导致内存泄漏

?所以说,当类中涉及到资源管理时,赋值运算符一定要由用户重载。

注意事项:

我们可以重载赋值运算符,不论形参的类型是什么,赋值运算符都必须定义为成员函数。

——《C++ Prime(第五版)》

?进一步了解赋值运算符重载

在对赋值运算符重载的时候,往往需要注意以下四点:

1.参数类型

2.返回值类型

3.检测是否自己给自己赋值

4.返回*this

让我们根据代码示例来分析要点:

date& operator=(date& d) {
    if (this != &d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    cout << "operator=" << endl;
    return *this;
}

1.参数类型

参数类型采用传引用或者传值对于结果来说,影响不大,但是,传引用的高效率却无法不令我们优先考虑。

2.返回值类型?

运算符重载需要保证其含义不改变。对于内置类型变量而言,赋值运算符是可以进行连续赋值的

a = b = c;

?同时,我们还需要知道,我们针对类类型对象所写的赋值语句,与赋值运算符的重载有着如下关系:

d1 = d2;
d1.operator(d2);

所以,对于连续赋值的情况:

d1 = d2 = d3;
//从右往左赋值
d1.operator(d2.operator(d3));

虽然赋值运算符重载函数是对象的成员函数,在函数体内对成员变量的修改是通过隐式传入的this指针完成的,所以,即使没有返回值,单次的赋值也是没有问题的。

但问题在于,连续赋值的情况下,我们是将d2.operator(d3)的值赋给d1,如果重载函数的返回值类型为void,那么d1.operator(void)显然是没有对应的可用重载的,势必会报错。

同时,由于返回值不是引用类型时,返回值是右值,从而导致在某些情况下无法做到连续赋值的功能,所以我们将函数的返回值设置为类类型引用

由于返回的是我们this指针所指向的对象,所以也不存在访问非法空间的问题。

3.检测是否自己为自己赋值。

同样是为了效率,在大型项目中,我们很难保证不会出现使用对象1的引用给对象1赋值的情况,这样的情况明显会对效率造成一些浪费。

所谓积水成渊,从任何可能的代码处节省时间应该称为我们所必备的意识。

4.返回*this

?取地址运算符重载&&const取地址运算符重载

class date {
private:
    int _year;
    int _month;
    int _day;
public:
    date* operator&() {
        return this;
    }

    const date* operator&() const {
        return this;
    }
};

这两者我实在想不出有什么特殊的情况需要对它进行什么修改的。

实践中也是如此,大部分博客中都认为这两个操作符一般不用用户来定义重载,使用编译器默认生成的取地址重载即可。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-26 11:24:29  更:2022-04-26 11:27:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 21:52:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码