C++复习:类和对象
类和对象
基本概念
1)类、对象、成员变量、成员函数
2)面向对象三大概念
封装、继承、多态
3)编程实践
类的定义和对象的定义,对象的使用
求圆形的面积
定义Teacher类,打印Teacher的信息(把类的声明和类的实现分开)
类的封装
1)封装(Encapsulation)
A)封装,是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。
B)封装,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
备注:有2层含义(把属性和方法进行封装 对属性和方法进行访问控制)
C++中类的封装
成员变量,C++中用于表示类属性的变量
成员函数,C++中用
于表示类行为的函数
2)类成员的访问控制
在C++中可以给成员变量和成员函数定义访问级别
Public(公有)修饰成员变量和成员函数可以在类的内部和类的外部被访问
Private(私有)修饰成员变量和成员函数只能在类的内部被访问
//类是把属性和方法封装 同时对信息进行访问控制 //类的内部,类的外部 //我们抽象了一个类,用类去定义对象 //类是一个数据类型,类是抽象的 //对象是一个具体的变量。。占用内存空间。 class { public: double r; double s; public: double getR() { a++; return r; } void setR(double { r = val; } public: double getS() //增加功能时,是在修改类, 修改类中的属性或者是方法 { s = 3.14f*r*r; return s; } //private: int a; }; |
3)struct和class关键字区别
在用struct定义类时,所有的没有被访问控制符限制的成员的默认属性为public
在用class定义类时,所有的没有被访问控制符限制的成员的默认属性为private
对象的构造和析构
前言
创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的。
为了解决这个问题,C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。
构造和析构函数
1构造函数和析构函数的概念
有关构造函数
1构造函数定义及调用
1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
2)构造函数在定义时可以有参数;
3)没有任何返回类型的声明。
2构造函数的调用
自动调用:一般情况下C++编译器会自动调用构造函数
手动调用:在一些情况下则需要手工调用构造函数
有关析构函数
3)析构函数定义及调用
1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
语法:~ClassName()
2)析构函数没有参数也没有任何返回类型的声明
3)析构函数在对象销毁时自动被调用
- 4)析构函数调用机制
C++编译器自动调用
2 C++编译器构造析构方案 PK 对象显示初始化方案
设计构造函数和析构函数的原因
面向对象的思想是从生活中来,手机、车出厂时,是一样的。
生活中存在的对象都是被初始化后才上市的;初始状态是对象普遍存在的一个状态的
普通方案:
为每个类都提供一个public的initialize函数;
对象创建后立即调用initialize函数进行初始化。
优缺点分析
1)initialize只是一个普通的函数,必须显示的调用
2)一旦由于失误的原因,对象没有初始化,那么结果将是不确定的
没有初始化的对象,其内部成员变量的值是不定的
3)不能完全解决问题
//为什么对象需要初始化 有什么样的初始化方案 #include using
/* 思考为什么需要初始化 面向对象思想来自生活,手机、车、电子产品,出厂时有初始化 怎么样进行初始化?
方案1:显示调用方法 缺点:易忘、麻烦;显示调用init,不能完全解决问题 */ class { public: int m; int getM() const { return m; } void setM(int
int n; int getN() const { return n; } void setN(int
public: int init(int { this->m = m; this->n = n; return 0; } protected: private: };
int main() { Test21 t1; //无参构造函数的调用方法 Test21 t2;
//t1.init(100, 200); //t2.init(300, 400); cout << t1.getM() << cout << t2.getM() <<
//定义对象数组时,没有机会进行显示初始化 Test21 arr[3]; //Test arr_2[3] = {Test(1,3), Test(), Test()};
system("pause"); return 0; } |
构造函数的分类及调用
C++编译器给程序员提供的对象初始化方案,高端大气上档次。
//有参数构造函数的三种调用方法 class { private: int a; int b;
public:
//无参数构造函数 Test() { ; }
//带参数的构造函数 Test(int { ; } //赋值构造函数 Test(const { ; }
public: void init(int { a = _a; b = _b; } }; |
1无参数构造函数
调用方法: Test t1, t2;
2有参构造函数
有参构造函数的三种调用方法
//有参数构造函数的三种调用方法 class { private: int a; public: //带参数的构造函数 Test5(int { printf("\na:%d", a); } Test5(int { printf("\na:%d b:%d", a, b); } public: };
int main() { Test5 t1(10); //c++编译器默认调用有参构造函数 括号法 Test5 t2 = (20, 10); //c++编译器默认调用有参构造函数 等号法 Test5 t3 = Test5(30); //程序员手工调用构造函数 产生了一个对象 直接调用构造函数法
system("pause"); return 0; } |
3拷贝构造函数调用时机
赋值构造函数的四种调用场景(调用时机)
第1和第2个调用场景
#include using
class { public: AA() //无参构造函数 默认构造函数 { cout << } AA(int { a = _a; } AA(const { cout << a = obj2.a + 10; } ~AA() { cout << } void getA() { printf("a:%d \n", a); } protected: private: int a; }; //单独搭建一个舞台 void ObjPlay01() { AA a1; //变量定义
//赋值构造函数的第一个应用场景 //用对象1 初始化 对象2 AA a2 = a1;
a2 = a1; //用a1来=号给a2 编译器给我们提供的浅拷贝 }
|
第二个应用场景 //单独搭建一个舞台 void ObjPlay02() { AA a1(10); //变量定义
//赋值构造函数的第一个应用场景 //用对象1 初始化 对象2 AA a2(a1);
//a2 = a1; //用a1来=号给a2 编译器给我们提供的浅拷贝 a2.getA(); } //注意:初始化操作 和 等号操作 是两个不同的概念 |
第3个调用场景
#include using
class { public: Location(int { X = xx; Y = yy; cout << } Location(const { X = p.X; Y = p.Y; cout << } ~Location() { cout << X << } int GetX() { return X; } int GetY() { return Y; } private: int X, Y; };
void f(Location { cout << }
void mainobjplay() { Location A(1, 2); //形参是一个元素,函数调用,会执行实参变量初始化形参变量 f(A); //采用值传递传递参数,所以发生拷贝。 }
void main() { mainobjplay(); system("pause"); }
|
第4个调用场景
第四个应用场景 #include using class Location { public: Location(int xx = 0, int yy = 0) { X = xx; Y = yy; cout << "Constructor Object.\n"; } Location(const Location & p) //复制构造函数 { X = p.X; Y = p.Y; cout << "Copy_constructor called." << endl; } ~Location() { cout << X << "," << Y << " Object destroyed." << endl; } int GetX() { return X; } int GetY() { return Y; } private: int X, Y; };
void f(Location p) { cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; }
Location g() { Location A(1, 2); return A; }
//对象初始化操作 和 =等号操作 是两个不同的概念 //匿名对象的去和留,关键看,返回时如何接 void mainobjplay() { //若返回的匿名对象,赋值给另外一个同类型的对象,那么匿名对象会被析构 //Location B; //B = g(); //用匿名对象 赋值 给B对象,然后匿名对象析构
//若返回的匿名对象,来初始化另外一个同类型的对象,那么匿名对象会直接转成新的对象 Location B = g(); cout << "测试" << endl; }
void main() { mainobjplay(); system("pause"); } |
4默认构造函数
两个特殊的构造函数
1)默认无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
2)默认拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制
构造函数调用规则研究
1)当类中没有定义任何一个构造函数时,c++编译器会提供默认无参构造函数和默认拷贝构造函数
2)当类中定义了拷贝构造函数时,c++编译器不会提供无参数构造函数
3) 当类中定义了任意的非拷贝构造函数(即:当类中提供了有参构造函数或无参构造函数),c++编译器不会提供默认无参构造函数
4 )默认拷贝构造函数成员变量简单赋值
总结:只要你写了构造函数,那么你必须用。
构造析构阶段性总结
1)构造函数是C++中用于初始化对象状态的特殊函数
2)构造函数在对象创建时自动被调用
3)构造函数和普通成员函数都遵循重载规则
4)拷贝构造函数是对象正确初始化的重要保证
5)必要的时候,必须手工编写拷贝构造函数
深拷贝和浅拷贝
- 默认复制构造函数可以完成对象的数据成员值简单的复制
- 对象的数据资源是由指针指示的堆时,默认赋值构造函数仅作指针值赋值
1浅拷贝问题分析
深拷贝浅拷贝现象出现的原因
2浅拷贝程序C++提供的解决方法
显式提供拷贝构造函数
显式重载=号操作,不使用编译器提供的浅拷贝
class { public: Name(const { size = strlen(pname); pName = (char *)malloc(size + 1); strcpy(pName, pname); } Name(Name &obj) { //用obj来初始化自己 pName = (char *)malloc(obj.size + 1); strcpy(pName, obj.pName); size = obj.size; } ~Name() { cout << "开始析构" << endl; if (pName != NULL) { free(pName); pName = NULL; size = 0; } }
void { if (pName != NULL) { free(pName); pName = NULL; size = 0; } cout << "测试有没有调用我。。。。" << endl;
//用obj3来=自己 pName = (char *)malloc(obj3.size + 1); strcpy(pName, obj3.pName); size = obj3.size; }
protected: private: char *pName; int size; };
//对象的初始化 和 对象之间=号操作是两个不同的概念 void playObj() { Name obj1("obj1....."); Name obj2 = obj1; //obj2创建并初始化
Name obj3("obj3...");
//重载=号操作符 obj2 = obj3; //=号操作
cout << "业务操作。。。5000" << endl;
} void main() { playObj(); system("pause"); } |
多个对象构造和析构
1对象初始化列表
1)对象初始化列表出现原因
1.必须这样做:
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,
如果没有初始化列表,那么他将无法完成第一步,就会报错。
2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,
因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
2)C++中提供初始化列表对成员变量进行初始化
语法规则
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)
{
// 其他赋值操作
}
3)注意概念
初始化:被初始化的对象正在创建
赋值:被赋值的对象已经存在
4)注意:
数据成员顺序最好和初始化列表的顺序一样
初始化列表先于构造函数的函数体执行
/* 1 C++中提供了初始化列表对成员变量进行初始化 2 使用初始化列表出现原因: 1.必须这样做: 如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数, 而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数, 如果没有初始化列表,那么他将无法完成第一步,就会报错。
2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值 当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化, 因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。 */
//总结 构造和析构的调用顺序
#include using
class ABC { public: ABC(int a, int b, int c) { this->a = a; this->b = b; this->c = c; printf("a:%d,b:%d,c:%d \n", a, b, c); printf("ABC construct ..\n"); } ~ABC() { printf("a:%d,b:%d,c:%d \n", a, b, c); printf("~ABC() ..\n"); } protected: private: int a; int b; int c; };
class MyD { public: MyD() :abc1(1, 2, 3), abc2(4, 5, 6), m(100) //MyD() { cout << "MyD()" << endl; } ~MyD() { cout << "~MyD()" << endl; }
protected: private: ABC abc1; //c++编译器不知道如何构造abc1 ABC abc2; const };
int run() { MyD myD; return 0; }
int main() {
run(); system("pause"); return 0; } |
构造函数和析构函数的调用顺序研究
构造函数与析构函数的调用顺序
1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数
2)析构函数的调用顺序与对应的构造函数调用顺序相反。
构造函数和析构函数综合
通过训练,把所学知识点都穿起来
1构造析综合训练
注意赋值构函数的调用
2匿名对象强化训练
- 匿名对象生命周期
- 匿名对象的去和留
3匿名对象强化训练
- 构造中调用构造
构造函数中调用构造函数,是一个蹩脚的行为。
对象的动态建立和释放
1 new和delete基本语法
1)在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和销毁内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。
注意: new和delete是运算符,不是函数,因此执行效率高。
2)虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。
new运算符的例子:
new int; //开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针)
new int(100); //开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址
new char[10]; //开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址
new int[5][4]; //开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址
float *p=new float (3.14159); //开辟一个存放单精度数的空间,并指定该实数的初值为//3.14159,将返回的该空间的地址赋给指针变量p
3)new和delete运算符使用的一般格式为:
用new分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。
- 应用举例
2 类对象的动态建立和释放
使用类名定义的对象都是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。
C++中,可以用new运算符动态建立对象,用delete运算符撤销对象
比如:
Box *pt; //定义一个指向Box类对象的指针变量pt
pt=new Box; //在pt中存放了新建对象的起始地址在程序中就可以通过pt访问这个新建的对象。
如
cout<<pt->height; //输出该对象的height成员
cout<<pt->volume( ); //调用该对象的volume函数,计算并输出体积C++还允许在执行new时,对新建立的对象进行初始化。
如
Box *pt=new Box(12,15,18);
这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。
新对象中的height,width和length分别获得初值12,15,18。调用对象既可以通过对象名,也可以通过指针。
在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。只要检测返回值是否为0,就可判断分配内存是否成功。
ANSI C++标准提出,在执行new出现故障时,就"抛出"一个"异常",用户可根据异常进行有关处理。但C++标准仍然允许在出现new故障时返回0指针值。当前,不同的编译系统对new故障的处理方法是不同的。
在不再需要使用由new建立的对象时,可以用delete运算符予以释放。如
delete pt; //释放pt指向的内存空间。这就撤销了pt指向的对象。此后程序不能再使用该对象。
如果用一个指针变量pt先后指向不同的动态对象,应注意指针变量的当前指向,以免删错了对象。在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。
建议一个指针专门只指向一个对象。
3 编程实践
//1 malloc free函数 c关键字
// new delete 操作符号 c++的关键字
//2 new 在堆上分配内存 delete
//分配基础类型 、分配数组类型、分配对象
//3 new和malloc 深入分析
混用测试、异同比较
结论: malloc不会调用类的构造函数
free不会调用类的析构函数
静态成员变量成员函数
思考:每个变量,拥有属性。有没有一些属性,归所有对象拥有?
静态成员变量
1)定义静态成员变量
- 关键字 static 可以用于说明一个类的成员,
静态成员提供了一个同类对象的共享机制
- 把一个类的成员说明为 static 时,这个类无论有多少个对象被创建,这些对象共享这个 static 成员
- 静态成员局部于类,它不是对象成员。(一个通用特性)
例如:
#include<iostream>
using
namespace std;
class
counter
{
static
int num; //声明与定义静态数据成员
public:
void setnum(int
i) { num = i; } //成员函数访问静态数据成员
void shownum() { cout << num <<
'\t'; }
};
int
counter::num = 0;//声明与定义静态数据成员
void main()
{
counter a, b;
a.shownum(); //调用成员函数访问私有静态数据成员
b.shownum();
a.setnum(10);
a.shownum();
b.shownum();
}
从结果可以看出,访问的是同一个静态数据成员
2)使用静态成员变量
// 使用公有静态数据成员
#include<iostream.h>
class counter
{
public:
counter(int a) { mem = a; }
int mem; //公有数据成员
static
int Smem; //公有静态数据成员
};
int counter::Smem = 1; //初始值为1
void main()
{
counter c(5);
int i;
for (i = 0; i < 5; i++)
{
counter::Smem += i;
cout << counter::Smem << '\t'; //访问静态成员变量方法2
}
cout << endl;
cout << "c.Smem = " << c.Smem << endl; //访问静态成员变量方法1
cout << "c.mem = " << c.mem << endl;
}
静态成员函数
1)概念
- 静态成员函数数冠以关键字static
- 静态成员函数提供不依赖于类数据结构的共同操作,它没有this指针
- 在类外调用静态成员函数用 "类名 :: "作限定词,或通过对象调用
2)案例
3)疑难问题:静态成员函数中,不能使用普通变量。只能调用同样属性的静态变量
//静态成员变量属于整个类的,分不清楚,是那个具体对象的属性。
综合
C++面向对象模型初探
前言
C++对象模型可以概括为以下2部分:
1. 语言中直接支持面向对象程序设计的部分,主要涉及如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等。
2. 对于各种支持的底层实现机制。
在c语言中,"数据"和"处理数据的操作(函数)"是分开来声明的,也就是说,语言本身并没有支持"数据和函数"之间的关联性。在c++中,通过抽象数据类型,在类中定义数据和函数,来实现数据和函数直接的绑定。
概括来说,在C++类中有两种成员数据:static、nonstatic(非静态);三种成员函数:static、nonstatic、virtual。
基础知识
C++中的class从面向对象理论出发,将变量(属性)和函数(方法)集中定义在一起,用于描述现实世界中的类。从计算机的角度,程序依然由数据段和代码段构成。 C++编译器如何完成面向对象理论到计算机程序的转化? 换句话:C++编译器是如何管理类、对象、类和对象之间的关系 具体的说:具体对象调用类中的方法,那,c++编译器是如何区分,是那个具体的类,调用这个方法那? |
思考一下程序结果 |
#include
using
class C1 { public: int i; //4字节 int j; //4字节 int k; //4字节 protected: private: }; //12字节
class C2 { public: int i; int j; int k;
static public: int getK() const { return k; } //4 void setK(int val) { k = val; } //4
protected: private: }; //24 16 12(都不对)
struct S1 { int i; int j; int k; }; //
struct S2 { int i; int j; int k; static }; //
int main() { printf("c1:%d \n", sizeof(C1)); printf("c2:%d \n", sizeof(C2)); printf("s1:%d \n", sizeof(S1)); printf("s2:%d \n", sizeof(S2));
system("pause"); } |
编译器对属性和方法的处理机制
通过上面的案例,我们可以的得出: 1)C++类对象中的成员变量和成员函数是分开存储的 成员变量: 普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式 静态成员变量:存储于全局数据区中 成员函数:存储于代码段中。 问题出来了:很多对象共用一块代码?代码是如何区分具体对象的那?
换句话说:int getK() const { return k; },代码是如何区分,具体obj1、obj2、obj3对象的k值? |
2)C++编译器对普通成员函数的内部处理 |
请仔细思考,并说出你的总结! |
总结
1、C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效! 2、C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。 3、静态成员函数、成员变量属于类 静态成员函数与普通成员函数的区别 静态成员函数不包含指向具体对象的指针 普通成员函数包含一个指向具体对象的指针 |
this指针
实验1:若类成员函数的形参 和 类的属性,名字相同,通过this指针来解决。
实验2:类的成员函数可通过const修饰,请问const修饰的是谁?(函数)
全局函数PK成员函数
1、把全局函数转化成成员函数,通过this指针隐藏左操作数
Test add(Test &t1, Test &t2)===》Test add(Test &t2)
2、把成员函数转换成全局函数,多了一个参数
void printAB()===》void printAB(Test *pthis)
3、函数返回元素和返回引用
Test& add(Test &t2) //*this //函数返回引用
{
this->a = this->a + t2.getA();
this->b = this->b + t2.getB();
return *this; //*操作让this指针回到元素状态
}
Test add2(Test &t2) //*this //函数返回元素
{
//t3是局部变量
Test t3(this->a + t2.getA(), this->b + t2.getB());
return t3;
}
友元
友元函数
例如
class A1 { public: A1() { a1 = 100; a2 = 200; } int getA1() { return } //声明一个友元函数 friend
protected: private: int a1; int a2; };
void setA1(A1 *p, int a1) { p->a1 = a1; } void main() { A1 mya1; cout << mya1.getA1() << endl; setA1(&mya1, 300); //通过友元函数 修改A类的私有属性 cout << mya1.getA1() << endl;
system("pause"); } |
友元类
- 若B类是A类的友员类,则B类的所有成员函数都是A类的友员函数
- 友员类通常设计为一种对数据操作或类之间传递消息的辅助类
强化
static关键字强化
- 某商店经销一种货物。货物购进和卖出时以箱为单位,各箱的重量不一样,因此,商店需要记录目前库存的总重量。现在用C++模拟商店货物购进和卖出的情况。
#include using
class Goods { public: Goods(int w) { weight = w; total_weight += w; } ~Goods() { total_weight -= weight; } int Weight() { return weight; }; static Goods *next; private: int weight; static };
int Goods::total_weight = 0;//静态变量的初始化别忘了
//r尾部指针 void purchase(Goods * &f, Goods *& r, int w) { Goods *p = new Goods(w); p->next = NULL; if (f == NULL) f = r = p; else { r->next = p; r = r->next; } //尾部指针下移或新结点变成尾部结点 } void sale(Goods * & f, Goods * & r) { if (f == NULL) { cout << "No any goods!\n"; return; } Goods *q = f; f = f->next; delete q; cout << "saled.\n"; }
void main() { Goods * front = NULL, *rear = NULL; int w; int choice; do { cout << "Please choice:\n"; cout << "Key in 1 is purchase,\nKey in 2 is sale,\nKey in 0 is over.\n"; cin >> choice; switch (choice) // 操作选择 { case 1: // 键入1,购进1箱货物 { cout << "Input weight: "; cin >> w; purchase(front, rear, w); // 从表尾插入1个结点 break; } case 2: // 键入2,售出1箱货物 { sale(front, rear); break; } // 从表头删除1个结点 case 0: break; // 键入0,结束 } cout << "Now total weight is:" << Goods::TotalWeight() << endl; } while (choice); }
|
小结
- 类通常用关键字class定义。类是数据成员和成员函数的封装。类的实例称为对象。
- 结构类型用关键字struct定义,是由不同类型数据组成的数据类型。
- 类成员由private, protected, public决定访问特性。public成员集称为接口。
- 构造函数在创建和初始化对象时自动调用。析构函数则在对象作用域结束时自动调用。
- 重载构造函数和复制构造函数提供了创建对象的不同初始化方式。
- 静态成员是局部于类的成员,提供一种同类对象的共享机制。
- 友元用关键字friend声明。友员是对类操作的一种辅助手段。一个类的友员可以访问该类各种性质的成员。
- 链表是一种重要的动态数据结构,可以在程序运行时创建或撤消数据元素。
运算符重载
概念
什么是运算符重载
所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是"一名多用"。
运算符也可以重载。实际上,我们已经在不知不觉之中使用了运算符重载。例如,大 家都已习惯于用加法运算符"+"对整数、单精度数和双精度数进行加法运算,如5+8, 5.8 +3.67等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的, 但由于C++已经对运算符"+"进行了重载,所以就能适用于int, float, doUble类型的运算。
又如"<<"是C++的位运算中的位移运算符(左移),但在输出操作中又是与流对象cout 配合使用的流插入运算符,">>"也是位移运算符(右移),但在输入操作中又是与流对象 cin 配合使用的流提取运算符。这就是运算符重载(operator overloading)。C++系统对"<<"和">>"进行了重载,用户在不同的场合下使用它们时,作用是不同 的。对"<<"和">>"的重载处理是放在头文件stream中的。因此,如果要在程序中用"<< "和">>"作流插入运算符和流提取运算符,必须在本文件模块中包含头文件stream(当然还应当包括"using namespace std")。
现在要讨论的问题是:用户能否根据自己的需要对C++已提供的运算符进行重载,赋予它们新的含义,使之一名多用?
运算符重载入门技术推演
1为什么会用运算符重载机制
用复数类举例
//Complex c3 = c1 + c2;
//原因 Complex是用户自定义类型,编译器根本不知道如何进行加减
//编译器给提供了一种机制,让用户自己去完成,自定义类型的加减操作。。。。。
//这个机制就是运算符重载机制
2 运算符重载的本质是一个函数
class Complex { public: int a; int b; friend Complex operator+(Complex &c1, Complex &c2); public: Complex(int a = 0, int b = 0) { this->a = a; this->b = b; }
public: void printCom() { cout << a << " + " << b << "i " << endl; }
private: };
/* Complex myAdd(Complex &c1, Complex &c2) { Complex tmp(c1.a+ c2.a, c1.b + c2.b); return tmp; } */
Complex { Complex tmp(c1.a + c2.a, c1.b + c2.b); return tmp; }
void main() { Complex c1(1, 2), c2(3, 4);
//Complex c3 = c1 + c2; //用户自定义类型 编译器无法让变量相加 //Complex myAdd(Complex &c1, Complex &c2);
//1 普通函数 //Complex c3 = myAdd(c1, c2); //c3.printCom();
//2 operator+ 函数名称 //Complex c3 = operator+(c1, c2); //c3.printCom();
//3 +替换 函数名 Complex c3 = c1 + c2; //思考C++编译器如何支持操作符重载机制的 (根据类型) c3.printCom(); { int a = 0, b = 0, c; //基础类型C++编译器知道如何加减 c = a + b; }
//4 把Complex类变成私有属性 //友元函数的应用场景 //friend Complex operator+(Complex &c1, Complex &c2);
cout << "hello..." << endl; system("pause"); return; } |
运算符重载的限制
运算符重载编程基础
例如:
//全局函数
完成 +操作符
重载
Complex operator+(Complex &c1, Complex &c2)
//类成员函数
完成 -操作符
重载
Complex operator-(Complex &c2)
运算符重载的两种方法
例如1:
//通过类成员函数完成-操作符重载
//函数声明 Complex operator-(Complex &c2)
//函数调用分析
//用类成员函数实现-运算符重载
Complex c4 = c1 - c2;
c4.printCom();
//c1.operator-(c2);
例如2:
//通过全局函数方法完成+操作符重载
//函数声明 Complex operator+(Complex &c1, Complex &c2)
//函数调用分析
int main()
{
Complex c1(1, 2), c2(3, 4);
//Complex c31 = operator+(c1, c2);
Complex c3 = c1 + c2;
c3.printCom();
}
例如3
//前置++操作符 用全局函数实现
Complex& operator++(Complex &c1)
{
c1.a ++;
c1.b ++;
return c1;
}
//调用方法
++c1 ; //=è需要写出操作符重载函数原形
c1.printCom();
//运算符重载函数名定义
//首先承认操作符重载是一个函数 定义函数名èoperator++
//分析函数参数 根据左右操作数的个数,èoperator++(Complex &c1)
//分析函数返回值è Complex& operator++(Complex &c1) 返回它自身
例如4
//4.1前置--操作符 成员函数实现
Complex& operator--()
{
this->a--;
this->b--;
return *this;
}
//4.2调用方法
--c1;
c1.printCom();
//4.3前置—运算符重载函数名定义
//c1.operator--()
例如5
//5.1 //后置++ 操作符 用全局函数实现
Complex operator++(Complex &c1, int)
{
Complex tmp = c1;
c1.a++;
c1.b++;
return tmp;
}
//5.2 调用方法
c1 ++ ; //先使用 后++
//5.3 后置++运算符重载函数名定义
Complex operator++(Complex &c1, int) //函数占位参数 和 前置++ 相区别
例如6
//6.1 后置— 操作符 用类成员函数实现
Complex operator--(int)
{
Complex tmp = *this;
this->a--;
this->b--;
return tmp;
}
//6.2 调用方法
c1 ++ ; //先使用 后++
//6.3 后置--运算符重载函数名定义
Complex operator--(int) //函数占位参数 和 前置-- 相区别
前置和后置运算符总结
C++中通过一个占位参数来区分前置运算和后置运算
定义运算符重载函数名的步骤
全局函数、类成员函数方法实现运算符重载步骤
1)要承认操作符重载是一个函数,写出函数名称operator+ ()
2)根据操作数,写出函数参数
3)根据业务,完善函数返回值(看函数是返回引用 还是指针 元素),及实现函数业务
友元函数实现操作符重载的应用场景
1)友元函数和成员函数选择方法
- 当无法修改左操作数的类时,使用全局函数进行重载
- =, [], ()和->操作符只能通过成员函数进行重载
2)用友元函数 重载 << >>操作符
- istream 和 ostream 是 C++ 的预定义流类
- cin 是 istream 的对象,cout 是 ostream 的对象
- 运算符 << 由ostream 重载为插入操作,用于输出基本类型数据
- 运算符 >> 由 istream 重载为提取操作,用于输入基本类型数据
- 用友员函数重载 << 和 >> ,输出和输入用户自定义的数据类型
a)用全局函数方法实现 << 操作符
ostream& operator<<(ostream &out, Complex &c1)
{
//out<<"12345,生活真是苦"<<endl;
out<<c1.a<<" + "<<c1.b<<"i "<<endl;
return out;
}
//调用方法
cout<<c1;
//链式编程支持
cout<<c1<<"abcc";
//cout.operator<<(c1).operator<<("abcd");
//函数返回值充当左值 需要返回一个引用
b)类成员函数方法无法实现 << 操作符重载
//因拿到cout这个类的源码
//cout.operator<<(c1);
- 友元函数重载操作符使用注意点
- 友员函数重载运算符常用于运算符的左右操作数类型不同的情况
b)其他
- 在第一个参数需要隐式转换的情形下,使用友员函数重载运算符是正确的选择
- 友员函数没有 this 指针,所需操作数都必须在参数表显式声明,很容易实现类型的隐式转换
- C++中不能用友员函数重载的运算符有
= () [] ->
4 )友元函数案例vector类
#include using
//为vector类重载流插入运算符和提取运算符 class vector { public: vector(int size = 1); ~vector(); int & operator[](int i); friend ostream & operator << (ostream & output, vector &); friend istream & operator >> (istream & input, vector &); private: int * v; int len; };
vector::vector(int size) { if (size <= 0 || size > 100) { cout << "The size of " << size << " is null !\n"; abort(); } v = new }
vector :: ~vector() { delete[] v; len = 0; }
int &vector::operator[](int i) { if (i >= 0 && i < len) return v[i]; cout << "The subscript " << i << " is outside !\n"; abort(); } ostream & operator << (ostream & output, vector & ary) { for (int i = 0; i < ary.len; i++) output << ary[i] << " "; output << endl; return output; } istream & operator >> (istream & input, vector & ary) { for (int i = 0; i < ary.len; i++) input >> ary[i]; return input; }
void main() { int k; cout << "Input the length of vector A :\n"; cin >> k; vector A(k); cout << "Input the elements of vector A :\n"; cin >> A; cout << "Output the elements of vector A :\n"; cout << A; system("pause"); } |
运算符重载提高
1运算符重载机制
C++编译器是如何支持操作符重载机制的?
2重载赋值运算符=
- 赋值运算符重载用于对象数据的复制
- operator= 必须重载为成员函数
- 重载函数原型为:
类型 & 类名 :: operator= ( const 类名 & ) ;
案例:完善Name类,支持=号操作。
结论:
1 //先释放旧的内存
2 返回一个引用
3 =操作符从右向左
//obj3 = obj1; // C++编译器提供的 等号操作 也属 浅拷贝 // obj4 = obj3 = obj1 //obj3.operator=(obj1)
Name& operator=(Name &obj1) { //1 先释放obj3旧的内存 if (this->m_p != NULL) { delete[] m_p; m_len = 0; } //2 根据obj1分配内存大小 this->m_len = obj1.m_len; this->m_p = new
//3把obj1赋值给obj3 strcpy(m_p, obj1.m_p); return *this; }
|
3重载数组下表运算符[]
重载[]和()运算符
- 运算符 [] 和 () 是二元运算符
- [] 和 () 只能用成员函数重载,不能用友元函数重载
重载下标运算符 []
[] 运算符用于访问数据对象的元素
重载格式 类型 类 :: operator[] ( 类型 ) ;
设 x 是类 X 的一个对象,则表达式
x [ y ]
可被解释为
x . operator [ ] ( y )
4重载函数调用符 ()
() 运算符用于函数调用
重载格式 类型 类 :: operator() ( 表达式表 ) ;
例1
设 x 是类 X 的一个对象,则表达式
x ( arg1, arg2, … )
可被解释为
x . operator () (arg1, arg2, … )
案例:
//例2:用重载()运算符实现数学函数的抽象
#include <iostream>
class F
{ public :
double operator ( ) ( double x , double y ) ;
} ;
double F :: operator ( ) ( double x , double y )
{ return x * x + y * y ; }
void main ( )
{
F f ;
f.getA();
cout << f ( 5.2 , 2.5 ) << endl ; //
f . operator() (5.2, 2.5)
}
比较普通成员函数
//例3 用重载()运算符实现 pk 成员函数
#include <iostream.h>
class F
{ public :
double memFun ( double x , double y ) ;
} ;
double F :: memFun ( double x , double y )
{ return x * x + y * y ; }
void main ( )
{
F f ;
cout << f.memFun ( 5.2 , 2.5 ) << endl ;
}
5为什么不要重载&&和||操作符
理论知识:
1)&&和||是C++中非常特殊的操作符
2)&&和||内置实现了短路规则
3)操作符重载是靠函数重载来完成的
4)操作数作为函数参数传递
5)C++的函数参数都会被求值,无法实现短路规则
#include #include
using
class Test { int i; public: Test(int i) { this->i = i; }
Test operator+ (const Test& obj) { Test ret(0);
cout << "执行+号重载函数" << endl; ret.i = i + obj.i; return ret; }
bool { cout << "执行&&重载函数" << endl; return i && obj.i; } };
// && 从左向右 void main() { int a1 = 0; int a2 = 1;
cout << "注意:&&操作符的结合顺序是从左向右" << endl;
if (a1 && (a1 + a2)) { cout << "有一个是假,则不在执行下一个表达式的计算" << endl; }
Test t1 = 0; Test t2 = 1;
If(t1 && (t1 + t2)) { //t1 && t1.operator(t2) // t1.operator( t1.operator(t2) ) cout << "两个函数都被执行了,而且是先执行了+" << endl; }
system("pause"); return; } |
附录:运算符和结合性
总结
操作符重载是C++的强大特性之一
操作符重载的本质是通过函数扩展操作符的语义
operator关键字是操作符重载的关键
friend关键字可以对函数或类开发访问权限
操作符重载遵循函数重载的规则
操作符重载可以直接使用类的成员函数实现
=, [], ()和->操作符只能通过成员函数进行重载
++操作符通过一个int参数进行前置与后置的重载
C++中不要重载&&和||操作符
C++复习:类和对象的更多相关文章
- 3.JAVA基础复习——JAVA中的类与对象
什么是对象: 就是现实中真实的实体,对象与实体是一一对应的,现实中每一个实体都是一个对象在. JAVA中的对象: Java中通过new关键字来创建对象. 类: 用JAVA语言对现实生活中的事物进行描述 ...
- Java复习(二)类与对象的基本概念
2.1面向对象的程序设计方法概述 对象 程序中: 一切皆是对象 都具有标识,属性和行为 通过一个或多个变量来保存其状态 通过方法实现他的行为 类 将属性及行为相同或相似的对象归为一类 类可以看成是对象 ...
- C#学习笔记(十一):类和对象
面向对象 为什么要面向对象: 1.和函数一样,把算法封装起来,方便复用 2.更好理解自己和别人写的代码 封装:数据.结构.逻辑的封装,方便复用 多态:同一个对象,同一种指令,不同的行为(反应) 继承: ...
- java 类与对象基础整理
之前学习javaSE的时候,没有针对性地对对类与对象的一些基础进行整理,下面这些内容是笔记内容整理后的,希望以后自己可以通过这些博客时常复习! 一.类and对象的基础 类似于类的生命啊,类与对象的关系 ...
- 关于Java构造类与对象的思考
简单记录一下Java构造类与对象时的流程以及this和super对于特殊例子的分析. 首先,接着昨天的问题,我做出了几个变形: Pic1.原版: Pic2.去掉了T.foo方法中的this关键字: P ...
- Java编程里的类和对象
像我们搞计算机这块的,都知道这么一件事,当前的计算机编程语言主要分为两大块,一为面向过程,二为面向对象.Java就是一门纯面向对象的语言.学习了一个月左右的Java,在下对于Java当中的类和对象有了 ...
- Python - 类与对象的方法
类与对象的方法
- C++基础知识(5)---类和对象
终于把C++中的基础在前面的几篇博客中总结完了,可能还有一些语法还没有总结到,没关系,以后用到了再查资料就好.类是C++中的一个非常重要的概念,这是区别你使用的C++到底是面向过程还是面向对象的一个重 ...
- 简述JavaScript对象、数组对象与类数组对象
问题引出 在上图给出的文档中,用JavaScript获取那个a标签,要用什么办法呢?相信第一反应一定是使用document.getElementsByTagName('a')[0]来获取.同样的,在使 ...
随机推荐
- html地址--待更新
11.学习笔记: 视频直播技术:ijkplayer技术:jni技术: https://www.cnblogs.com/renhui/category/1011048.html: IM:环信, xmpp ...
- java.util.base64报错解决
java.util.Base64 这个类,它是在 JDK 1.8 的时候加入的,之前版本的标准库没有这个类. eclipse更换jdk1.8就可以了了.
- [UE4]使用C++重写蓝图,SpawnObject根据类型动态创建UObject
先大量使用蓝图制作项目,后续再用C++把复杂的蓝图重写一遍,用C++代码按照蓝图依葫芦画瓢就可以了,很简单,但需要遵守一些原则: 第一种方法:使用继承 一.创建一个C++类作为蓝图的父类(C++类继承 ...
- 挂载本地iso镜像
挂载本地iso镜像 [root@linux-node1 ~]# mkdir -p /disk/iso [root@linux-node1 ~]# cd /disk/iso/ [root@linux-n ...
- 18 LVM逻辑卷管理
根据上一节的内容,我们知道md这个内核模块可以用来做软RAID的管理.同时RAID实现了两个功能:1.提高了磁盘的读写能力:2.对于数据进行了冗余备份: 但是,如果是管理员手动误删的数据,则一样无法找 ...
- SQL Server 2016:内存列存储索引
作者 Jonathan Allen,译者 谢丽 SQL Server 2016的一项新特性是可以在“内存优化表(Memory Optimized Table)”上添加“列存储索引(Columnstor ...
- call与apply简单介绍
var pet={ word:'...', speak:function(say){ console.log(say+' '+this.word) } } //pet.speak('speak')// ...
- ajax事件执行顺序
1.ajaxStart(全局事件) 2.beforeSend 3.ajaxSend(全局事件) 4.success(请求成功时调用) 5.ajaxSuccess(全局事件) 6.error 7.aja ...
- canvas实现刮刮乐
效果展示 源码下载
- api文档管理系统合集
1.Swagger 2.Showdoc 3.EOAPI 4.阿里的RAP 5.postMan 6.docute: 无需编译的文档撰写工具 7.SmartWiki 接口文档在线管理系统 8.SosoAp ...