C++类&&对象的深入研究

没错,C_T开学了
亲爱的副教授视频上课,真开心啊


菜鸟教程之C++类&对象教程

先复习一下类&&对象函数的知识点

在这里插入图片描述

常见的数据类型有:

  • 基本数据类型
    • 整型,实型(单精度,双精度)
    • 字符型
  • 构造类型
    • 枚举类型
    • 数组类型
    • 结构体类型,联合类型,类
  • 引用
  • 指针类型
  • 空类型

而这里我们要重点讨论的是——

类由数据和处理数据的函数封装而成
类是一种可以 " 发展 " 的数据类型,即一个类可以派生出另外一个类,派生出来的类不仅具有原类的一切特征,还可具备扩充的新特征

在这里插入图片描述


预处理问题

预处理:在进行编译的第一遍词法扫描和语法分析之前所作的工作
预处理命令包括:#include#define

扩展知识:宏定义(4种)
宏由编译预处理程序执行

  • #define<宏名><字符串>
    字符串及注释中的相应值不会被替换,如#define PI 3.14
  • #define<宏名>(<参数表>)<字符串>
    出现<宏名>的地方用文字穿代替,<文字串>中参数(相当于形参)将被替换成<宏名>提供的参数(相当于实参),如果一行写不下,可在本行最后用续行符 ’ \ ’ 后跟一个回车转到下一行
  • #define<宏名>
    告知编译程序<宏名>已被定义,不做文本替换,实现条件编译
  • #undef<宏名>
    取消某个宏的定义,其后的<宏名>不再进行替换,不再有意义

条件编译

一般情况下,源程序中所有的行都参加编译
但是有时希望其中一部分内容只在满足一定条件时才进行不安逸,也就是对一部分内容指定编译的条件

#ifdef FLAG
cout<<""<<endl;
#endif

#ifndef FLAG
cout<<"wolrd"<<endl;
#endif
//------------------
#ifdef FLAG
cout<<"hello"<<endl;
#else
cout<<"world"<<endl;
#endif

为什么要介绍这个呢?
我们观察下面的这个栗子:
在这里插入图片描述

编译失败,提示:class A被重定义了
也就是说,#include的含义实际上是对引用的头文件里所有内容进行一次全新定义
如果不同的头文件中具有相同的定义,就会引发错误
怎么解决这个问题呢?
在这里插入图片描述
所以从今往后,我们建议

  • 所有头文件均使用预处理器封装,以避免重复的头文件引用
#ifndef CLASS_H
#define CLASS_H

#endif
  • 尽量将接口和实现分离,这样当添加新的类型进行程序扩展时,原有的操作接口代码不需要进行修改,可以使程序更易于扩展

创建对象

class Time{...};

Time sunset;                     //创建Time类的对象
Time arrayOfTimes[5];            //创建Time类对象的数组
Time &dinnerTime=sunset;         //对象sunset的引用
Time *timeptr1=&dinnerTime;      //指向对象的指针 
Time *timeptr2=&sunset;          //指向对象的指针

需要强调的是:只有在缺省构造函数的条件下,才可以创建某个类对象的数组,自定义的带参数构造函数是不行的


对象的占用空间

当我们申请一个对象的时候,ta到底占用了多少空间呢?

在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量,必须为该变量的类型所占用的字节数的倍数
VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后 ,还会根据需要自动填充空缺的字节。

class test{
private:
    int num;
}

int类型4个字节,num存储在1~4字节
所以一共需要4字节

class test{
private:
    int num;
    char s,c;
}

int类型4个字节,num存储在1~4字节
char类型1个字节,s存储在第5字节,c存储在第6字节
确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数
所以一共需要8字节

class test{
private:
    char s;
    int num;
    char c,t;
}

char类型1个字节,s存储在第1字节
int类型4个字节,由于存放的起始地址相对于结构的起始地址的偏移量,必须为该变量的类型所占用的字节数的倍数,所以num存储在5~8字节
c存储在第9字节,t存储在第10字节
确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数
所以一共需要12字节

class test{
private:
    int num;
    char s,c,t
}

int类型4个字节,num存储在1~4字节
char类型1个字节,s存储在第5字节,c存储在第6字节,t存储在第7字节
确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数
所以一共需要8字节(定义变量时的顺序不同,占用的空间就会发生变化)

class test{
private:
    static char s;
    int num;
}

static类型时静态存储类,依赖于全局内存空间,不计算入对象的占用空间中
int类型4个字节,num存储在1~4字节
所以一共需要4字节


作用域问题

作用域分为很多种:
函数作用域(Function Scope),全局作用域(File Scope),模块作用域(Block Scope),还有类作用域(Class Scope)

类里面定义的数据成员和成员函数都在类作用域之下
类内:可以调用所有数据成员和成员函数
类外:可以通过句柄调用public类型的数据成员和成员函数

句柄 handles
句柄调用主要有四种形式:

class Test{...};

Test member;
Test &memberRef = member;
Tset *memberPtr = &member;

member.set(1);
member.print();

memberRef.set(1);
memberRef.print();

(*memberPtr).set(3);
(*memberPtr).print();

memberPtr->set(4);
memberPtr->print();
  • 对象名称 + " . "
  • 对象的引用 + " . "
  • 用指针和 " * " 表示对象 + " . "
  • 对象指针 + " -> "

类中定义的数据成员对该类的每个对象都有一个拷贝
类中的成员函数对该类的所有对象只有一个拷贝
在这里插入图片描述
那么问题来喽!

类中成员函数对该类的所有对象只有一个拷贝, 被调用时如何知道是对那一个对象操作呢?

每一个成员函数都有一个隐藏的指针类型形参:this

  • <类名> *const this
  • 通过对象调用成员函数时,编译程序会把对象地址作为隐含参数传递给形参this
  • 访问thsi指向的对象时:this->
  • 一般情况下省略this

在这里插入图片描述


接口和实现分离

Class Code分为接口 ( Interface ) 和实现 ( Implementation ) 两部分,
一般情况下,接口 ( Interface ) 包括inline函数(内联函数)和私有数据成员等信息
内联函数通常直接在头文件中定义
private数据成员在头文件中定义,且只能在类内部使用

我们一再强调,将接口和实现分离
简单来讲,用户只能得知类内成员函数和数据成员的定义,但是并不知道其中的实现方法,这样就从一定程度上保护了开发者的知识产权

软件供应商在他们的产品中只需提供头文件和类库(目标模块),而不需要提供源代码

在这里插入图片描述


访问函数和工具函数

访问函数 (access function)

  • 用来读取或显示数据
  • 常见用法之一是测试条件的真假(判断函数):例如vector的empty函数,可以判断vector是否为空

工具函数 (utility function)

  • 类的private成员函数,从而避免被类的客户直接使用
  • 不属于类的public成员函数,用来支持类的public成员函数

一般情况下,类的数据成员和在类内部使用的成员函数应该被指定为private类型,只有提供给外界使用的成员函数才能指定为public类型
而操作一个对象时,只能通过访问对象中的public成员来实现
在这里插入图片描述


具有默认实参的构造函数

我们在函数讲解中提到了默认实参的处理方式:

int cal(int =0,int =0,int =0);

int cal(int x,int y,int z) {
    return x*y*z;
}

构造函数也是函数啊,所以也可以拥有默认实参,而且实现方法也基本一致
当然,有时候程序员并没有自定义构造函数
这种情况下,程序在执行的时候会偷摸地调用编译器内部的缺省构造函数

什么是缺省构造函数?
缺省构造函数是不带参数或者所有参数都是默认值的构造函数
简单来说,缺省构造函数就是不指定实参即可调用的构造函数

注意:

  • 函数重载 (overloading)
    C++要求每个函数的函数签名不同,即函数名称可以相同,但是参数列表要相异(变量数,类型,顺序)
    所以我们也可以对构造函数进行重载
  • 构造函数可以指定默认实参
    不带参数(或所有参数都是默认值)的构造函数称为缺省构造函数
    缺省构造函数可以在不提供任何参数的情况下,仍能够初始化数据成员,保证对象的数据成员处于正常状态
  • 只要类中提供了自定义构造函数,编译器就不再提供缺省构造函数
  • 构造函数通过调用其他成员函数对数据成员初始化
    如果在构造函数内部直接给数据成员赋值,那就叫赋值,不叫初始化
  • 类的成员函数对数据成员的访问尽量都通过set和get函数实现
    一方面增强了函数的模块化程度
    另一方面方便了对访问操作的统一修改

类的应用实例:高精度加减


析构函数

析构函数是一个特殊的成员函数,名字同类名,并在前面加 " ~ " 字符,用来与构造函数加以区别

注意:

  • 析构函数不指定数据类型,也没有参数

  • 一个类中只能定义一个析构函数,析构函数不能重载

  • 析构函数可以被调用,也可以由系统调用。
    以下两种情况,析构函数会被自动调用:

    • 如果一个对象被定义在一个函数体内,当这个函数结束时,该对象的析构函数会被自动调用
    • 当一个对象是使用new运算符被动创建的,在使用delete运算符释放它时,delete将会自动调用析构函数

与构造函数的区别:

  • 构造函数是成员函数,函数体可写在类体内,也可写在类体外
  • 函数名与类名相同,该函数不指定类型说明,有隐含的返回值,该值由系统内部使用
  • 该函数可以有若干个参数,也可以没有参数
  • 构造函数可以被重载
  • 程序不能直接调用构造函数,只能在创建对象时由系统自动调用

下面我们要讨论一个很厉害的话题:

析构函数的调用法则

还记得存储类别吗?不记得也没关系,看这张图快速想起来!在这里插入图片描述

当然啦,对象也有各种各样的存储类别,针对不同的存储类别,析构函数的调用时间是不同的

  • 全局变量:在任何函数(含main)执行前构造;在程序结束时析构
  • 局部变量:
    • 自动变量:对象定义时构造,块结束时析构
    • 静态变量:首次定义时构造,程序结束时析构(静态变量存在于整个程序的运行期间,所以实际上依赖于全局内存,属于全局数据)
  • 全局对象,静态对象(均为静态存储类别)析构顺序恰好与构造顺序相反:先构造的对象后析构(栈操作)
特殊情况一:调用exit函数退出程序执行时,不调用剩余对象的析构函数
特殊情况二:调用abort函数退出程序执行时,不调用任何剩余对象的析构函数
#include<string>
using std::string;

#ifndef CREATE_H
#define CREATE_H
class CreateAndDestroy {
public:
	CreateAndDestroy(int,string);
	~CreateAndDestroy();
private:
	int ID;
	string message;
};
#endif
//CreateAndDestroy.cpp
#include<iostream>
#include<string>
#include "CreateAndDestroy.h"
using namespace std;

CreateAndDestroy::CreateAndDestroy(int x,string s) {
	ID=x;
	message=s;
	cout<<"Create "<<ID<<" ("<<message<<")\n";
}

CreateAndDestroy::~CreateAndDestroy() {
	cout<<"Destroy "<<ID<<"("<<message<<")\n";
}
//Test.cpp
#include<iostream>
#include<string>
#include "CreateAndDestroy.h"
using namespace std;

CreateAndDestroy first(1,"global");

void create() {
	CreateAndDestroy fifth(5,"local");
	static CreateAndDestroy sixth(6,"static in local");
	CreateAndDestroy seventh(7,"local");
}

int main() 
{
	CreateAndDestroy second(2,"main");
	static CreateAndDestroy third(3,"static in main");
	create();
	CreateAndDestroy fourth(4,"main");
	return 0;
}
// Output
Create 1 (global)
Create 2 (main)
Create 3 (static in main)
Create 5 (local)
Create 6 (static in local)
Create 7 (local)
Destroy 7 (local)
Destroy 5 (local)
Create 4 (main)
Destroy 4 (main)
Destroy 2 (main)
Destroy 6 (static in local)
Destroy 3 (static in main)
Destroy 1 (global)

在这里插入图片描述


返回引用

函数返回引用这个知识点我记得我由学过,但是我找不到我学过ta的证据。。。
下面这个栗子会有一点难以理解,我就简单说几句提示一下吧:

  • 只要调用了test()就会产生输出,一共六次调用,对应着六个输出
  • test()返回的是静态局部变量val的引用,因此val_ref就是val本人,对val_ref的赋值就是对val的修改
  • 当然啦,val_ref只是一个变量名,目的就是方便我们对引用进行操作
    test()本身返回的就是一个引用,所以直接对test()赋值也可以实现对val的修改
#include<iostream>
using namespace std;

int &test() {
	static int val=0;    
	//想要返回局部变量的引用,就必须是静态局部变量
	val++;
	cout<<val<<endl;
	return val;
}

int main() 
{
	test(); test(); test();
	int &val_ref=test();
	val_ref=100;
	test()=200;
	test();
	system("pause");
	return 0;
}

// Output
1
2
3
4
101
201

类的成员函数也可以返回私有数据成员的引用,但是这是一种非常危险的行为
假若如此,用户就能够对类中的私有数据成员直接进行修改和调用
这就和我们想方设法 " 将接口和实现分开 " 背道而驰了


拷贝构造函数

最难的知识点来咯!!!

类的对象支持一般意义的赋值操作

class Data{
private:
    int x,y,z;
};

Data A,B;
A=B;
// equal to
A.x=B.x;
A.y=B.y;
A.z=B.z;

但是以下两种情况不是赋值,而是调用拷贝构造函数

void copyData(Data B);
Data A;
copyDate(A);

情况一:在函数传参的时候,Data B的定义就调用了拷贝构造函数,将A的信息拷贝到了B中

Data A;
Data B=A;

情况二:在定义对象时,直接进行初始化Data B=A时调用了拷贝构造函数

这两种情况有一个重要的共同之处:本质是用对象初始化对象

一般情况下,编译器会提供缺省拷贝构造函数,将原对象的每个数据成员的值拷贝到新对象的相应成员
当然我们也可以自定义拷贝构造函数,语法也很简单:类名称(类名称 & 形参)

class Data{
public:
    Data(int a=0,int b=0,int c=0) {setData(a,b,c);}
    ~Data() {cout<<"Dastroy data";}
    Data(Data &t) {
        setData(t.x,t.y,t.z);
        cout<<"Copy Constructor is called.\n";
    }
    void setData(int a,int b,int c) {x=a;y=b;z=c;}
private:
    int x,y,z;
};

一般情况下,我们是不用特意构造拷贝构造函数,编译器的缺省拷贝构造函数就可以满足我们的需求
但是缺省拷贝构造函数有一个致命的缺点:数据成员为指针时会出现错误
为什么?
我们可以这样理解:拷贝构造函数在一定程度上可以认为是简单的内容拷贝
所以数据成员为指针时,拷贝构造函数会直接将同一个地址赋值给新对象的指针
这就造成了一个十分尴尬的局面:新旧两个对象的指针都指向了同一个地址
对其中一个对象的指针所指内容修改,就会牵连另一个对象的内容
在调用析构函数时,同一块内存会被delete释放两次,导致程序异常

class test{
public:
    test(int n) {    
        pArray = new int[n];
    }
    ~test() {delete []pArray;}
private:
    int *pArray;
};

怎么解决这个问题?
那是下节课的内容啦 * ★°*:.☆( ̄▽ ̄)/$:*.°★ *

发布了964 篇原创文章 · 获赞 235 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/wu_tongtong/article/details/104479914