类类型 C++支持的内置类型和操作,如 int i=10; i=i%6; i=i+4; 怎样创建用户自定义类型和该类型支持的操作? 如创建一个复数类型,支持的操作有+,-,*,/等 complex i; i=i*2; //支持*,即实部和虚部分别*2
示例代码
第四章 类与对象 C++: C with Classes 封装(Encapsulation)是面向对象程序设计最基本的特性,也就是把数据(属性)和函数(操作)合成一个整体,这是用类与对象实现的。接口(类设计)和实现(编程)分离。 本章重点: 1.引入C++的类(class)和对象(object)的概念,建立“函数也可以是数据类型的成员”的思想。 2.运算符重载。
第四章 类与对象 4.1 类与对象 4.6 友元 4.2 从面向过程到面向对象 4.7 静态成员 4.3 构造函数和析构函数 4.8 结构 第四章 类与对象 4.1 类与对象 4.6 友元 4.2 从面向过程到面向对象 4.7 静态成员 4.3 构造函数和析构函数 4.8 结构 4.4 引用与复制构造函数 4.9 名字空间域和类域(选读) 4.5 运算符的重载 4.10面向对象的程序设计和Windows编程
4.1 类与对象 4.1.1 C++类的定义 4.1.2 成员函数的定义 4.1.3对象的创建与使用
4.1.1 C++类的定义 类是一种(抽象/用户自定义)数据类型。 用不同的数据类型来描述客观事物的不同的属性。如商品: 类的引入: 类是一种(抽象/用户自定义)数据类型。 用不同的数据类型来描述客观事物的不同的属性。如商品: 商品名称(string),该商品数量(unsigned),该商品单价(float)。 这里用了属于三种不同数据类型的三个数据成员(data member)来描述一种商品。
4.1.1 C++类的定义 课下查找:利用关键字class和struct定义类的区别? 关键字class是数据类型说明符; class(struct) CGoods{ private : string m_name ; int m_amount ; float m_price ; public : //成员函数声明 void register(string,int,float); float getTotalValue() ; string getName() ; int getAmount() ; float getPrice() ; float sell(int num); }; //最后的分号不可少 关键字class是数据类型说明符; 标识符CGoods是商品这个类的类型名; 课下查找:利用关键字class和struct定义类的区别? 数据成员 成员函数(操作)
4.1.1 C++类的定义 访问限定符(access specifier): public(公共的)说明的成员能从外部(类声明和定义代码外面)进行访问。 private(私有的)说明的成员不能从外部进行访问。 每种说明符可在类体中使用多次。 访问限定符的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前结束。 如果在类体起始点无访问说明符,系统默认定义为私有(private用关键字class定义类)。
4.1.2 成员函数的定义 成员函数定义: 返回值类型 类名::函数名(参数表) {……}//函数体 4.1.2 成员函数的定义 成员函数定义: 通常在类定义中,成员函数仅作声明。函数定义通常在类的定义之后进行,其格式如下: 返回值类型 类名::函数名(参数表) {……}//函数体 其中运算符“::”称为作用域解析运算符(scope resolution operator),它指出该函数是属于哪一个类的成员函数。 类CGoods的函数定义
4.1.3 对象的创建与使用 定义对象: 对象是类的实例(instance)。 格式如下: CGoods mobile; 4.1.3 对象的创建与使用 定义对象: 对象是类的实例(instance)。 格式如下: CGoods mobile; 这个定义创建了CGoods类的一个对象mobile,同时为它分配了属于它自己的存储块,用来存放数据和对这些数据实施操作的成员函数(代码)。
4.1.3 对象的创建与使用 对象使用规则: 只要在对象名后加点号(点操作符,成员访问运算符(member access oprator)之一),再加成员数据或成员函数名就可以了。 【例4.1】中对象mobile的4个数据成员全是私有的,如写: int main(){ ... cout<<mobile.m_name; // error, m_name为私有数据成员,不能类外访问 cout<<mobile.getName() ;//ok }
4.3 构造函数和析构函数 特殊的成员函数,只要创建类类型的对象,都要执行构造函数。功能:为数据成员分配存储空间并初始化每个对象的数据成员。 4.3 构造函数和析构函数 特殊的成员函数,只要创建类类型的对象,都要执行构造函数。功能:为数据成员分配存储空间并初始化每个对象的数据成员。 构造函数(constructor) 4.3.1 构造函数的定义与使用 4.3.2 析构函数的定义
4.3.1 构造函数的定义与使用 构造函数特征: 1.函数名与类名相同,可以重载。 class CGoods{ public: 4.3.1 构造函数的定义与使用 构造函数特征: 1.函数名与类名相同,可以重载。 class CGoods{ public: CGoods (string name , int amount , float price); CGoods (int amount , float price); }; 2.构造函数无函数返回类型说明。注意是什么也不写,包括void! 3. 当对象被建立,该对象所属的类的构造函数自动被调用,在该对象的生命期中也只调用这一次。 { CGoods book1; }
4.3.1 构造函数的定义与使用 4.构造函数可以在类中定义,也可以在类外定义 class CGoods{ public: 4.3.1 构造函数的定义与使用 4.构造函数可以在类中定义,也可以在类外定义 class CGoods{ public: CGoods (string name , int amount , float price){ //类内部定义 m_name=name ;m_amount=amount ;m_price=price ; } }; 或者 CGoods (string name , int amount , float price);//类内部声明 CGoods::CGoods (string name , int amount , float price){//类外部定义
5. 如果类说明中没有给出构造函数,则C++编译器自动创建一个默认的构造函数: 类名() {} class CGoods{ public: CGoods(); //默认 CGoods(int); //用户自定义 }; 5. 如果类说明中没有给出构造函数,则C++编译器自动创建一个默认的构造函数: 类名() {} 注意:只要我们定义了一个构造函数,系统就不会自动生成默认的构造函数。 只要构造函数是无参的或各参数均有默认值的,C++编译器都认为是默认的构造函数,并且默认的构造函数只能有一个 。
练习 请为CGoods定义一种带参数的默认构造函数 class CGoods{ public: CGoods(float price=0); }; class CGoods{ public: CGoods(float price=0,int amount=0); };
4.3.1 构造函数的定义与使用 CGoods的构造函数: 三参数: 4.3.1 构造函数的定义与使用 CGoods的构造函数: 三参数: CGoods (string name , int amount , float price){ m_name=name;m_amount=amount ; m_price=price ; } 两参数:货名和单价, CGoods (string name , float price){ m_name=name; m_price=price ; m_amount=0; } 默认的构造函数: CGoods(){ m_price=0; m_amount=0; } 这三个构造函数同时被说明(重载)。
4.3.1 构造函数的定义与使用 实参决定调用哪个构造函数: 【例4.1_1】完整商品类对象应用实例 4.3.1 构造函数的定义与使用 实参决定调用哪个构造函数: CGoods Car1(“Audi A8”,30,98000.0); int a(4); 调用了CGoods中的第一个构造函数,等效于: CGoods Car2(“Ferrari F12”,164000.0) ; 调用的是第二个构造函数,参数为两个。 CGoods moblie; 定义时调用不带参数的构造函数(不能加括号) 例如:CGoods moblie(); // int fun(); 定义了一个返回值为类CGoods对象且不带参数的函数,函数名moblie 【例4.1_1】完整商品类对象应用实例
课下练习 选择以下一个抽象,确定类中需要什么数据,并提供适当的构造函数。并解释你的决定。 1. Book 2. Date 3. Student 4. Vehicle 5. Tree 6. Computer 7. Program
在类的定义中初始化数据成员 class CGoods{ private : string m_name ; int m_amount =0; //C++11 float m_price=0 ; };
构造函数初始化式 与普通函数一样,构造函数具有名字,形参列表和函数体。 不同:定义时可以包含一个初始化列表,不能在声明处指出 CGoods ::CGoods (string name , int amount , float price):m_name(name),m_amount(amont),m_price(price){ } 用例:const类型数据成员 class A{ private: const int i; public: A(int var){i=var;} //error A(int var):i(var){ } };
成员初始化的次序: 数据成员定义的次序决定了初始化的次序.初始化列表只是仅初始化成员的值,而不指定初始化的顺序。 class X{ int i; int j; public: //run-time error: i is initialized before j X(int val):j(val),i(j){ } }; X a(10);
4.3.2 析构函数的定义 析构函数(destructor)特征: 【例4.2】定义一个矩形类 4.3.2 析构函数的定义 析构函数(destructor)特征: 当一个对象的生命周期结束时,C++会自动调用析构函数(destructor),销毁该对象。 1. 构函数名与类名相同,但在前面加上字符‘~’,如 ~CGoods()。 2. 析构函数无函数返回类型,与构造函数在这方面是一样的。但析构函数不带任何参数。 3. 一个类有一个也只有一个析构函数,这与构造函数不同。析构函数可以默认。 4. 对象注销时,系统自动调用析构函数,按成员在类中声明的次序逆序撤销成员(释放存储空间)。 【例4.2】定义一个矩形类
4.4 引用与复制构造函数 4.4.1 引用 4.4.2 复制构造函数 4.4.3 成员对象与构造函数
4. 4.1 引用 引用的导入: 参数传递的传值方式在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中。它的优点是有效避免改变实参值。 问题:1.如果要求改变实参的值,怎么办呢? 2.如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢? 有一种数据类型—引用(reference)可以解决上面的难题。引用又称别名(alias)。
4.4.1 引用 引用的定义: 类型 &对象名 = 已定义过的左值对象; 例如: double number ; 4.4.1 引用 引用的定义: 引用是给一个已经定义的左值对象重新起一个别名,而不是定义一个新的变量,定义的格式为: 类型 &对象名 = 已定义过的左值对象; 例如: double number ; double &refnum=number ; refnum是number 的引用,即refnum是number的别名。
4.4.1 引用 refnum与number绑定,C++不会将number的内容复制给refnum,它们共享内存,见下图: 4.4.1 引用 refnum与number绑定,C++不会将number的内容复制给refnum,它们共享内存,见下图: number称为引用newnum的关联变量。“&”在这里是引用的说明符。必须注意number和newnum都是double类型。如在程序中修改了newnum也就是修改了number,两位一体。 refnum number = 24; printf(“refnum=%d”,refnum); // 打印输出:refnum=24 refnum=48; printf(“number=%d”,number); // 打印输出:number=48
4.4.1 引用 引用只能引用左值对象,不能引用右值或临时对象,所以引用通常称作左值引用 int i(0); 4.4.1 引用 引用只能引用左值对象,不能引用右值或临时对象,所以引用通常称作左值引用 int i(0); int &r1=i+4; //不能引用临时对象 int & r2=24; //不能引用字面值常量
const 引用(不严谨):指向const 对象的引用 const int val=1024; const int &refval=val; refval=14; //错误 int &ref2=val; // 错误非const 引用指向const对象 课下查找,以下语句是否合法,并解释: int i=34; double j=65.14; const int &ref1=34; int &ref3=24; const int &ref2=ref1+i; int &ref4=ref1+i; const int &ref5=j; 可参考C++ Primer(第五版) p55
注意:采用引用返回方式时,返回的不能是函数中的局部变量,这时返回的局部变量地址已经失效。 4.4.1 引用 【例4.3】引用作为函数的参数。 采用引用调用时,将对实参进行操作。 引用作为函数的返回值 double & fsqr(double a){ double result=a*a ; return result;} 可以吗? int main(){ double x=fsqr(5); printf(“x=%f\n”,x); } 注意:采用引用返回方式时,返回的不能是函数中的局部变量,这时返回的局部变量地址已经失效。
复制构造函数: 4.4.2 复制构造函数 int a=3; int b=a; 4.4.2 复制构造函数 class CGoods{ string m_name ; int m_amount ; float m_price ; public: CGoods(const CGoods &); } 复制构造函数: int a=3; int b=a; string str1="copy"; string str2=str1; 在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为Copy Constructor。 CGoods::CGoods(const CGoods & cgd) :m_name (cgd.m_name), m_price(cgd.m_price), m_amount(cgd. m_amount) { } 默认复制构造函数形参应该声明为引用: CGoods(const CGoods & cgd); 思考: 为什么?
4.4.2 复制构造函数 实例: CGood Car1("Peugeot 508",30,98000.00); //调用三个参数的构造函数 4.4.2 复制构造函数 实例: CGood Car1("Peugeot 508",30,98000.00); //调用三个参数的构造函数 CGood Car2= Car1; //调用复制构造函数 Car2=Car1; //调用重载的赋值操作符(后面介绍) CGood Car3 ( Car1); //调用复制构造函数,Car1为实参 这样三个对象的初始化结果完全一样。
4.4.2 复制构造函数 复制构造函数的调用: 当函数的形参是非引用类型,调用函数时,进行形参与实参结合时使用。这时要在栈中新建立一个局部对象(形参),并把实参复制到新的对象中,即调用拷贝构造函数。 void copy1(CGoods car){ .... } CGoods car1("balala",33); copy1(car1);
CGoods copy2(const CGoods &g){ CGoods car; car.name=g.name; .... 2.当函数的返回值是非引用类型,函数执行完成返回调用者时使用。要建立一个临时对象,再返回调用者。 CGoods copy2(const CGoods &g){ CGoods car; car.name=g.name; .... return car; } CGoods g1,g2, ... g1=copy2(g2);
4.5 运算符的重载 运算符重载的概念: 运算符重载函数定义: int a(1),b(2),c; c=a+b; Complex x,y,z; z=x+y; 4.5 运算符的重载 运算符重载的概念: 运算符的重载是特殊的函数重载,必须定义一个函数,并通知C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,通常为类的成员函数。 运算符重载函数定义: 返回值类型 类名::operator重载的运算符(参数表) {……} operator是关键字,它与重载的运算符一起构成函数名。因函数名的特殊性,C++编译器可以将这类函数识别出来。
细解运算符重载: 4.5 运算符的重载 复数类+的重载: Complex c2(1,1),c3(1,1),c1; 4.5 运算符的重载 class Complex{ double m_real,m_image; public: Complex(double,double ); Complex operator+(Complex ); }; 细解运算符重载: 复数类+的重载: Complex c2(1,1),c3(1,1),c1; c1=c2+c3; ---> c1=c2.operator+(c3) ; Complex Complex::operator+(Complex c){ //显式定义局部对象 Complex Temp(Real+c.Real , Image+c.Image) ; return Temp ; } 思考:此语句被调用到执行完毕,执行了几次构造函数?能否提高执行效率?
返回值优化: 省略局部的Complex对象Temp Complex Complex::operator+(Complex c){ Complex t(m_real+c.m_real , m_image+c.m_image) ; return t ; }//如何改进? 返回值优化: 省略局部的Complex对象Temp Complex Complex::operator+(const Complex &c){ return Complex(m_real+c.m_real , m_image+c.m_image); } 在return后面跟的表达式中调用的是类的构造函数,它为无名对象赋值(初始化),返回值就是该无名对象。 改进后的代码调用一次构造函数
调用operator=, 如 a.operator=(b); class Complex{ double m_real,m_image; public: Complex(double,double ); Complex& operator=(const Complex &); }; 2. 重载赋值操作符: Complex a,b(1,1); a = b; Complex &Complex::operator = (const Complex& c){ m_real=c.m_real; image=c.m_image; return *this; } 调用operator=, 如 a.operator=(b); 【例4.8】复数类,应用它进行复数运算
4.5 运算符的重载 小结: 1. 运算符重载函数的函数名必须为关键字operator加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。 2. 当用类的成员函数实现运算符的重载时,运算符重载函数的参数(当为双目运算符时)为一个或(当为单目运算符时)没有。运算符的左操作数一定是对象.
4.5 运算符的重载 3. 优先级和结合性是固定的 x==y+z; x.operator==(y.operator+(z))
4.5 运算符的重载 问题:double d=05; Complex c; 怎样解决? 例5.7中: c=c+d; 语句,改为 c=d+c; 4.5 运算符的重载 问题:double d=05; Complex c; 例5.7中: c=c+d; 语句,改为 c=d+c; 因为d不是Complex的对象,C++编译器将无法找到合适的重载的“+”运算符对应的函数,最终给出出错信息。 怎样解决?
4.6 友元 在C++中友元(friend)函数允许在类外访问该类中的任何成员,就象成员函数一样。友元函数用关键字friend说明。 4.6 友元 在C++中友元(friend)函数允许在类外访问该类中的任何成员,就象成员函数一样。友元函数用关键字friend说明。 上节答案: 用友元函数重载运算符“+”,可以实现 c=d+c;
4.6 友元 //d+c被C++编译器解释为:operator+(d,c) 4.6 友元 Complex operator + (double d , const Complex& c){ return Complex(d+c.m_real , c.m_image) ; } class Complex {…… friend Complex operator + (double,const Complex&); }; friend只用于类说明中,定义时不加friend,opration + : 类Complex的友元函数,可以访问类的私有成员,非成员函数 int main( ){ …… c=d+c; //d+c被C++编译器解释为:operator+(d,c)
4.6 友元 友元函数重载运算符形式: +有三种形式。另两个的声明为: 涵盖实数与复数,复数与复数,复数与实数相加三种情况。 4.6 友元 友元函数重载运算符形式: +有三种形式。另两个的声明为: friend Complex operator +(const Complex&, const Complex&) ; friend Complex operator + (const Complex&, double ) ; 涵盖实数与复数,复数与复数,复数与实数相加三种情况。 思考:仅用上面第一个友元函数(类中无重载+),能否完成以下操作 Complex operator +(const Complex&c1, const Complex&c2){ return Complex(c1.real+c2.real,c1.image+c2.image); } Complex c1,c2; double d1; c2=c1+d; c2=d+c1;
作用域与命名 int a; int g_a; //全局对象 class X{ class X{ int m_a; //数据成员 X(){ int a; a=1; } }; int g_a; //全局对象 class X{ int m_a; //数据成员 X(){ int a; //局部对象 } };
总结 1. 类:用户自定义类型,数据和操作的封装 2. 通过对象访问数据成员和成员函数 3. 定义一个类X,编译器定义的默认成员函数 class X{}; 1). X(){}; 2). ~X(){}; 3). X(const X&){} 4). X& operator=(const X&){};
上机和作业 作业: 无 上机考试: 实验十二第三题(参考Complex类)
第五章 类与对象 结束 谢谢!
4.1.2 成员函数的定义 void CGoods::register(string name , int amount , float price){ m_name =name ; m_amount=amount ; m_price=price ; } float CGoods::getTotalValue(){ return m_price*m_amount; string CGoods::getName(){ return m_name; ...
【例4.1】商品类对象应用实例 【例4.1】商品类对象应用实例: #include<iostream> #include<iomanip> #include<string> using namespace std; //省略了类定义 int main( ){ CGoods car ; string str ; int number ; float pr ;
cout<<“请输入汽车型号:” ; getline(cin,str) ; cin>>number>>pr ; car.registerGoods(str , number , pr) ; car.CountTotal() ; str=car.getName() ; //获得car.Name cout<<setw(20)<<str<<setw(5) <<car.getAmount() ; //A cout<<setw(10)<<car.GetPrice()<<setw(20) <<car.GetTotalValue()<<endl ; //B return 0; } 对象car Name ; Amount ; Price ; Total_value ; minicar 5 2 str number Pr minicar minicar minicar 5 2 10 5 2 10
关于数据成员命名:m_前缀
【例4.2】矩形类 【例4.2】矩形类。要确定一个矩形(四边都是水平或垂直方向,不能倾斜),只要确定其左上角和右下角的x和y坐标即可,即左右上下四个边界值。
【例4.2】矩形类
【例4.2】矩形类
【例4.3】引用作为形参 void swap(double & d1, double & d2){ double t ; t = d1 ; d2 = t ; } int main(void){ double x , y ; cout<<"请输入x和y的值" <<'\n'; cin>>x>>y ; swap(x,y) ; cout<<"x="<<x<<'\t' <<"y="<<y<<'\n'; return 0; } 图5.5 参数d1、d2为引用时内存分配示意 X y 1.414 2.718 d1 d2 t 2.718 1.414 1.414
【例4.4】 引用作为返回值 【例4.4】采用不同返回方式的求正方形面积函数的比较。 double result; //全局变量 double fsqr1(double a){ result=a*a ; return result;} double & fsqr2(double a){ int main(){ double x=fsqr1(5.5); //第一种情况 double y=fsqr2(5.5); //第二种情况 cout<<"x="<<x<<'\t‘<<"y="<<y<<endl; return 0;} 运行结果为: x=30.25 y=30.25 运行结果一样,但在内存中的活动却不同。 double & fsqr2(double a){ double result=a*a ; return result;} 可以吗? 想一想,这个例子需要将全局的result返回吗?
【例4.6】含有成员对象的类的构造函数
【例4.6】含有成员对象的类的构造函数 Point类构造函数被调用了几次?分别是哪些?
【例4.7】演示对象创建和撤消的对应关系 本例目的是总结一下语法,请注意各函数输出的标志: class complex{ private: double real, image; public: complex(){ //默认的构造函数 real=0.0; image=0.0; cout<<"Initializing 0 0"<<endl;} complex(double r,double i=0.0){ //带参数的构造函数 real=r; image=i; cout<<"Initializing"<<r<<'\t'<<i<<endl;} complex(const complex &com); //复制的构造函数声明 ~complex(){ //析构函数 cout <<"Destructor"<<endl; }
【例4.7】演示对象创建和撤消的对应关系 void assign(complex com){ real=com.real; //先建立局部对象(非临时对象)com image=com.image; } void print(){ cout<<real<<'+'<<image<<'i'<<endl; } }; inline complex::complex(const complex &com){ //复制的构造函数说明 cout<<"Copy"<<com.real<<'\t‘ <<com.image<<endl; real=com.real; image=com.image; }
【例4.7】演示对象创建和撤消的对应关系 complex fun(complex com){ cout<<"Entering function"<<endl; global.assign(com); cout<<"Exiting function"<<endl; return global; } complex global; //全局对象首先建立 int main(){ cout <<"Entering main"<< endl; complex com1, com2(5.6, 7.5); complex com3=com1; com3.print(); global.print(); com1=fun(com2); com1.print(); cout<<"Exiting main"<<endl; return 0;}
【例4.7】演示对象创建和撤消的对应关系 运行结果: Initializing 0 0 //全局对象global建立,调默认的构造函数 complex fun(complex com); complex global; //全局对象首先建立 int main(){ cout <<"Entering main"<< endl; complex com1, com2(5.6, 7.5); complex com3=com1; com3.print(); global.print(); com1=fun(com2); com1.print(); cout<<"Exiting main"<<endl; return 0;} 【例4.7】演示对象创建和撤消的对应关系 运行结果: Initializing 0 0 //全局对象global建立,调默认的构造函数 Entering main //进入入口函数main Initializing 0 0 //用默认的构造函数建立com1 Initializing 5.6 7.5 //用带参数的构造函数建立com2 Copy 0 0 //用复制的构造函数建立com3 0+0i //打印com3 0+0i //打印global Copy 5.6 7.5 //调用fun(),调用复制构造函数建立局部(非临时)对象com Entering function //进入全局函数fun() Copy 5.6 7.5 //进入global.assign(), //调用复制构造函数建立局部(非临时)com
【例4.7】演示对象创建和撤消的对应关系 Destructor //退出global.assign(),调用析构函数,清新com complex fun(complex com); complex global; //全局对象首先建立 int main(){ cout <<"Entering main"<< endl; complex com1, com2(5.6, 7.5); complex com3=com1; com3.print(); global.print(); com1=fun(com2); com1.print(); cout<<"Exiting main"<<endl; return 0; } 【例4.7】演示对象创建和撤消的对应关系 Destructor //退出global.assign(),调用析构函数,清新com Exiting function //将退出fun() Copy 5.6 7.5 //返回对象时调用复制构造函数建立临时对象 Destructor //退出fun(),调用析构函数,清fun()的com Destructor //返回的临时对象赋给com1后析构 5.6 + 7.5i //打印com1 Exit main //将退出入口函数main Destructor //退出入口函数前,调用析构函数,清com3 Destructor //退出入口函数前,调用析构函数,清com2 Destructor //退出入口函数前,调用析构函数,清com1 Destructor //退出入口函数后,调用析构函数,清global 课下练习:本例运行结果应与程序对比,看看程序运行的细节。
【例4.8】复数类 验证主函数
【例4.8】复数类
【例4.8】复数类