没错,C_T开学了
和亲爱的副教授视频上课,真开心啊
常见的数据类型有:
- 基本数据类型
- 整型,实型(单精度,双精度)
- 字符型
- 构造类型
- 枚举类型
- 数组类型
- 结构体类型,联合类型,类
- 引用
- 指针类型
- 空类型
而这里我们要重点讨论的是——
类
类由数据和处理数据的函数封装而成
类是一种可以 " 发展 " 的数据类型,即一个类可以派生出另外一个类,派生出来的类不仅具有原类的一切特征,还可具备扩充的新特征
预处理问题
预处理:在进行编译的第一遍词法扫描和语法分析之前所作的工作
预处理命令包括:#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;
};
怎么解决这个问题?
那是下节课的内容啦 * ★°*:.☆( ̄▽ ̄)/$:*.°★ *