1、C++对C的扩展
1简单的C++程序
1.1求圆的周长和面积
数据描写叙述:
半径。周长,面积均用实型数表示
数据处理:
输入半径 r。
计算周长 = 2*π*r ;
计算面积 = π* r2 。
输出半径,周长,面积;
方法1:用结构化方法编程,求圆的周长和面积
// count the girth and area of circle #include<iostream.h> using name std; void main () { double r, girth, area ; const double PI = 3.1415 ; cout << "Please input radius:\n" ; //操作符重载 cin >> r ; //输入 girth = 2 * PI * r ; area = PI * r * r ; cout << "radius = " << r << endl ; cout << "girth = " << girth << endl ; cout << "area = " << area << endl ; } |
方法2:用面向对象方法编程,求圆的周长和面积
#include<iostream.h> using name std; class Circle { double radius ; //成员变量 public : //类的訪问控制 void Set_Radius( double r ) { radius = r ; } //成员函数 double Get_Radius() { return radius ; } //通过成员函数设置成员变量 double Get_Girth() { return 2 * 3.14f * radius ; } //通过成员函数获取成员变量 double Get_Area() { return 3.14f * radius * radius ; } } ; void main() { Circle A, B ; //用类定义对象 A.Set_Radius( 6.23 ) ; //类的调用 cout << "A.Radius = " << A.Get_Radius() << endl ; cout << "A.Girth = " << A.Get_Girth() << endl ; cout << "A.Area = " << A.Get_Area() << endl ; B.Set_Radius( 10.5 ) ; cout << "B.radius = " << B.Get_Radius() << endl ; cout << "B.Girth=" << B.Get_Girth() << endl ; cout << "B.Area = " << B.Get_Area() << endl ; } |
总结:建立类、对象、成员变量、成员函数,输入输入流基本概念。
1.2刚開始学习的人易犯错误模型
// demo02_circle_err.cpp #include<iostream> using namespace std;//c++的命名空间 class circle { public: double r; double pi = 3.1415926; double area = pi*r*r; }; int main() { circle pi; cout << "请输入area" << endl; cin >> pi.r; cout << pi.area << endl; //乱码 system("pause"); return 0; } |
总结: 从内存四区的角度。解释为什么会出现乱码
理解为什么须要成员函数
2程序设计方法的发展历程
面向过程的结构化程序设计方法
l 设计思路
– 自顶向下、逐步求精。採用模块分解与功能抽象,自顶向下、分而治之。
l 程序结构:
– 按功能划分为若干个基本模块,形成一个树状结构。
– 各模块间的关系尽可能简单,功能上相对独立;每一模块内部均是由顺序、选择和循环三种基本结构组成。
– 其模块化实现的详细方法是使用子程序。
l 长处:
有效地将一个较复杂的程序系统设计任务分解成很多易于控制和处理的子任务,便于开发和维护。
l 缺点:可重用性差、数据安全性差、难以开发大型软件和图形界面的应用软件
– 把数据和处理数据的过程分离为相互独立的实体。
– 当数据结构改变时,全部相关的处理过程都要进行对应的改动。
– 每一种相对于老问题的新方法都要带来额外的开销。
– 图形用户界面的应用程序,非常难用过程来描写叙述和实现。开发和维护也都非常困难。
面向对象的方法
l 将数据及对数据的操作方法封装在一起,作为一个相互依存、不可分离的总体——对象。
l 对同类型对象抽象出其共性。形成类。
l 类通过一个简单的外部接口,与外界发生关系。
l 对象与对象之间通过消息进行通信。
面向对象的基本概念
对象
l 一般意义上的对象:
– 是现实世界中一个实际存在的事物。
– 能够是有形的(比方一辆汽车),也能够是无形的(比方一项计划)。
– 是构成世界的一个独立单位,具有
l 静态特征:能够用某种数据来描写叙述
l 动态特征:对象所表现的行为或具有的功能
l 面向对象方法中的对象:
– 是系统中用来描写叙述客观事物的一个实体。它是用来构成系统的一个基本单位。对象由一组属性和一组行为构成。
– 属性:用来描写叙述对象静态特征的数据项。
– 行为:用来描写叙述对象动态特征的操作序列。
类
l 分类——人类通常的思维方法
l 分类所根据的原则——抽象
– 忽略事物的非本质特征,仅仅注意那些与当前目标有关的本质特征。从而找出事物的共性。把具有共同性质的事物划分为一类,得出一个抽象的概念。
– 比如,石头、树木、汽车、房屋等都是人们在长期的生产和生活实践中抽象出的概念。
l 面向对象方法中的"类"
– 具有同样属性和服务的一组对象的集合
– 为属于该类的所有对象提供了抽象的描写叙述。包含属性和行为两个主要部分。
– 类与对象的关系:
宛如模具与铸件之间的关系,一个属于某类的对象称为该类的一个实例。
封装
也就是把客观事物封装成抽象的类,而且类能够把自己的数据和方法仅仅让可信的类或者对象操作。对不可信的进行信息隐藏。
l 把对象的属性和服务结合成一个独立的系统单元。
l 尽可能隐蔽对象的内部细节。对外形成一个边界(或者说一道屏障),仅仅保留有限的对外接口使之与外部发生联系。
l 继承对于软件复用有着重要意义,是面向对象技术可以提高软件开发效率的重要原因之中的一个。
l 定义:特殊类的对象拥有其一般类的所有属性与服务。称作特殊类对一般类的继承。
l 比如:将轮船作为一个一般类,客轮便是一个特殊类。
多态
多态是指在一般类中定义的属性或行为,被特殊类继承之后,能够具有不同的数据类型或表现出不同的行为。这使得同一个属性或行为在一般类及其各个特殊类中具有不同的语义。
面向对象的软件project
l 面向对象的软件project是面向对象方法在软件project领域的全面应用。它包含:
– 面向对象的分析(OOA)
– 面向对象的设计(OOD)
– 面向对象的编程(OOP)
– 面向对象的測试(OOT)
– 面向对象的软件维护(OOSM)
总结:
面向过程程序设计:数据结构 + 算法
主要解决科学计算问题,用户需求简单而固定
特点:
分析解决这个问题所须要的步骤
利用函数实现各个步骤
依次调用函数解决这个问题
问题:
软件可重用性差
软件可维护性差
构建的软件无法满足用户需求
面向对象程序设计:由现实世界建立软件模型
将现实世界中的事物直接映射到程序中,可直接满足用户需求
特点:
直接分析用户需求中涉及的各个实体
在代码中描写叙述现实世界中的实体
在代码中关联各个实体协同工作解决这个问题
优势:
构建的软件可以适应用户需求的不断变化
直接利用面向过程方法的优势而避开其劣势
3 C语言和C++语言关系
C语言是在实践的过程中逐步完好起来的 没有深思熟虑的设计过程 使用时存在非常多“灰色地带” 残留量过多低级语言的特征 直接利用指针进行内存操作 |
C语言的目标是高效 终于程序运行效率的高效 |
当面向过程方法论暴露越来越多的缺陷的时候,业界開始考虑在project项目中引入面向对象的设计方法。而第一个须要解决的问题就是:高效的面向对象语言,而且可以兼容已经存在的代码。 C语言 + 面向对象方法论===》Objective C /C++ |
C语言和C++并非对立的竞争关系 C++是C语言的加强。是一种更好的C语言 C++是以C语言为基础的。而且全然兼容C语言的特性 |
学习C++并不会影响原有的C语言知识,相反会依据加深对C的认知。 学习C++能够接触到很多其它的软件设计方法,并带来很多其它的机会。 1) C++是一种更强大的C,通过学习C++可以掌握很多其它的软件设计方法 2) C++是Java/C#/D等现代开发语言的基础,学习C++后可以高速掌握这些语言 3)C++是各大知名软件企业挑选人才的标准之中的一个 |
4 C++对C的加强
4.1 namespace命名空间
1 C++命名空间基本常识
所谓namespace,是指标识符的各种可见范围。
C++标准程序库中的全部标识符都被定义于一个名为std的namespace中。
一 :<iostream>和<iostream.h>格式不一样。前者没有后缀,实际上,在你的编译器include目录里面能够看到,二者是两个文件。打开文件就会发现,里面的代码是不一样的。后缀为.h的头文件c++标准已经明白提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件中,c++标准为了和C差别开,也为了正确使用命名空间。规定头文件不使用后缀.h。因此,
1)当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
2)当使用<iostream>的时候。该头文件未定义全局命名空间,必须使用namespace std;这样才干正确使用cout。
二: 因为namespace的概念,使用C++标准程序库的不论什么标识符时。能够有三种选择:
1、直接指定标识符。比如std::ostream而不是ostream。完整语句例如以下:std::cout << std::hex << 3.4 << std::endl;
2、使用usingkeyword。 usingstd::cout; using std::endl; using std::cin; 以上程序能够写成 cout<< std::hex << 3.4 << endl;
3、最方便的就是使用usingnamespace std; 比如: using namespace std;这样命名空间std内定义的全部标识符都有效(曝光)。
就好像它们被声明为全局变量一样。那么以上语句能够例如以下写: cout <<hex << 3.4 << endl;由于标准库非常的庞大,所以程序猿在选择的类的名称或函数名 时就非常有可能和标准库中的某个名字同样。
所以为了避免这样的情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。但这又会带来了一个新问题。
无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h>
和<iostream>等等这种头文件。一个是为了兼容曾经的C++代码。一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和曾经的头文件差别,一般不加".h"
2 C++命名空间定义及使用语法
/* 在C++中。名称(name)能够是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免,在大规模程序的设计中,以及在程序猿使用各种各样的C++库时,这些标识符的命名发生冲突。 标准C++引入了keywordnamespace(命名空间/名字空间/名称空间/名域)。能够更好地控制标识符的作用域。 */ |
/* std是c++标准命名空间,c++标准程序库中的全部标识符都被定义在std中,比方标准库中的类iostream、vector 等都定义在该命名空间中,使用时要加上using声明(using namespace std) 或using指示(如std::string、 std::vector<int>). */ |
/* C中的命名空间 在C语言中仅仅有一个全局作用域 C语言中全部的全局标识符共享同一个作用域 标识符之间可能发生冲突 C++中提出了命名空间的概念 命名空间将全局作用域分成不同的部分 不同命名空间中的标识符能够同名而不会发生冲突 命名空间能够相互嵌套 全局作用域也叫默认命名空间 */ |
/* C++命名空间的定义: namespace name { … } */ |
/* C++命名空间的使用: 使用整个命名空间:using namespace name; 使用命名空间中的变量:using name::variable; 使用默认命名空间中的变量:::variable 默认情况下能够直接使用默 认命名空间中的全部标识符 */ |
3 C++命名空间编程实践
namespace NameSpaceA { int a = 0; } namespace NameSpaceB { int a = 1; namespace NameSpaceC { struct Teacher { char name[10]; int age; }; } } int main() { using namespace NameSpaceA; using NameSpaceB::NameSpaceC::Teacher; printf("a = %d\n", a); printf("a = %d\n", NameSpaceB::a); NameSpaceB::NameSpaceC::Teacher t2 Teacher t1 = {"aaa", 3}; printf("t1.name = %s\n", t1.name); printf("t1.age = %d\n", t1.age); system("pause"); return 0; } |
4 结论
1) 当使用<iostream>的时候。该头文件未定义全局命名空间。必须使用namespace std;这样才干正确使用cout。
若不引入using namespace std ,须要这样做。std::cout。
2) c++标准为了和C差别开。也为了正确使用命名空间,规定头文件不使用后缀.h。
3) C++命名空间的定义: namespacename { … }
4) using namespace NameSpaceA;
5) namespce定义可嵌套。
4.2 “有用性”添加
#include "iostream" using namespace std; //C语言中的变量都必须在作用域開始的位置定义! 。 //C++中更强调语言的“有用性”,全部的变量都能够在须要使用时再定义。 int main11() { int i = 0; printf("ddd"); int k; system("pause"); return 0; } |
4.3 registerkeyword增强
//registerkeyword 请求编译器让变量a直接放在寄存器里面。速度快 //在c语言中 register修饰的变量 不能取地址,可是在c++里面做了内容 /* //1 registerkeyword的变化 registerkeyword请求“编译器”将局部变量存储于寄存器中 C语言中无法取得register变量地址 在C++中依旧支持registerkeyword C++编译器有自己的优化方式,不使用register也可能做优化 C++中能够取得register变量的地址 //2 C++编译器发现程序中须要取register变量的地址时,register对变量的声明变得无效。 //3 早期C语言编译器不会对代码进行优化。因此register变量是一个非常好的补充。 */ |
int main22() { register int a = 0; printf("&a = %x\n", &a); system("pause"); return 0; } |
其它补充:请阅读《registerkeyword常识课外阅读.docx》
4.4变量检測增强
/* 在C语言中,反复定义多个同名的全局变量是合法的 在C++中,不同意定义多个同名的全局变量 C语言中多个同名的全局变量终于会被链接到全局数据区的同一个地址空间上 int g_var; int g_var = 1; C++直接拒绝这样的二义性的做法。 */ |
int main(int argc, char *argv[]) { printf("g_var = %d\n", g_var); return 0; } |
4.5 struct类型加强
struct类型的加强: C语言的struct定义了一组变量的集合,C编译器并不觉得这是一种新的类型 C++中的struct是一个新类型的定义声明 |
struct Student { char name[100]; int age; }; int main(int argc, char *argv[]) { Student s1 = {"wang", 1}; Student s2 = {"wang2", 2}; return 0; } |
4.6 C++中全部的变量和函数都必须有类型
/* C++中全部的变量和函数都必须有类型 C语言中的默认类型在C++中是不合法的 函数f的返回值是什么类型,參数又是什么类型? 函数g能够接受多少个參数? */ //更换成.cpp试试 f(i) { printf("i = %d\n", i); } g() { return 5; } int main(int argc, char *argv[]) { f(10); printf("g() = %d\n", g(1, 2, 3, 4, 5)); getchar(); return 0; } |
总结:
/*
在C语言中
intf( )。表示返回值为int,接受随意參数的函数
intf(void);表示返回值为int的无參函数
在C++中
intf( );和int f(void)具有同样的意义,都表示返回值为int的无參函数
*/
C++更加强调类型。随意的程序元素都必须显示指明类型
4.2-4.6属于语法级别的增强。
4.7新增Bool类型keyword
/* C++中的布尔类型 C++在C语言的基本类型系统之上添加了bool C++中的bool可取的值仅仅有true和false 理论上bool仅仅占用一个字节, 假设多个bool变量定义在一起。可能会各占一个bit。这取决于编译器的实现 true代表真值,编译器内部用1来表示 false代表非真值,编译器内部用0来表示 bool类型仅仅有true(非0)和false(0)两个值 C++编译器会在赋值时将非0值转换为true,0值转换为false */ |
int main(int argc, char *argv[]) { int a; bool b = true; printf("b = %d, sizeof(b) = %d\n", b, sizeof(b)); b = 4; a = b; printf("a = %d, b = %d\n", a, b); b = -4; a = b; printf("a = %d, b = %d\n", a, b); a = 10; b = a; printf("a = %d, b = %d\n", a, b); b = 0; printf("b = %d\n", b); system("pause"); return 0; } |
4.8三目运算符功能增强
1三目运算符在C和C++编译器的表现
int main() { int a = 10; int b = 20; //返回一个最小数 而且给最小数赋值成3 //三目运算符是一个表达式 。表达式不可能做左值 (a < b ? a : b )= 30; printf("a = %d, b = %d\n", a, b); system("pause"); return 0; } |
2结论
1)C语言返回变量的值 C++语言是返回变量本身
C语言中的三目运算符返回的是变量值。不能作为左值使用
C++中的三目运算符可直接返回变量本身,因此能够出如今程序的不论什么地方
2)注意:三目运算符可能返回的值中假设有一个是常量值,则不能作为左值使用
(a < b ? 1 :b )= 30;
3)C语言怎样支持类似C++的特性呢?
====>当左值的条件:要有内存空间;C++编译器帮助程序猿取了一个地址而已
思考:怎样让C中的三目运算法当左值呢?
5 C/C++中的const
1 const基础知识(使用方法、含义、优点)
int main() { const int a; int const b; const int *c; int * const d; const int * const e ; return 0; } Int func1(const ) 0基础理解:const是定义常量==》const意味着仅仅读 |
含义: //第一个第二个意思一样 代表一个常整形数 //第三个 c是一个指向常整形数的指针(所指向的内存数据不能被改动,可是本身能够改动) //第四个 d 常指针(指针变量不能被改动。可是它所指向内存空间能够被改动) //第五个 e一个指向常整形的常指针(指针和它所指向的内存空间,均不能被改动) |
Const优点 //合理的利用const, //1指针做函数參数。能够有效的提高代码可读性,降低bug。 //2清楚的分清參数的输入和输出特性 |
int setTeacher_err( const Teacher *p) Const改动形參的时候。在利用形參不能改动指针所向的内存空间 |
2 C中“冒牌货”
int main() { const int a = 10; int *p = (int*)&a; printf("a===>%d\n", a); *p = 11; printf("a===>%d\n", a); printf("Hello......\n"); return 0; } |
解释: C++编译器对const常量的处理 当碰见常量声明时。在符号表中放入常量 =è问题:那有怎样解释取地址 编译过程中若发现使用常量则直接以符号表中的值替换 编译过程中若发现对const使用了extern或者&操作符。则给相应的常量分配存储空间(兼容C) ?联想: int &a = 1(err) & const int &a = 10(ok)? |
C++中const符号表原理图 |
注意: C++编译器尽管可能为const常量分配空间。但不会使用其存储空间中的值。 |
结论: C语言中的const变量 C语言中const变量是仅仅读变量,有自己的存储空间 C++中的const常量 可能分配存储空间,也可能不分配存储空间 当const常量为全局,而且须要在其他文件里使用 当使用&操作符取const常量的地址 |
3 const和#define同样之处
//练习 解释为什么 //#define N 10 int main() { const int a = 1; const int b = 2; int array[a + b ] = {0}; int i = 0; for(i=0; i<(a+b); i++) { printf("array[%d] = %d\n", i, array[i]); } getchar(); return 0; } |
C++中的const修饰的,是一个真正的常量,而不是C中变量(仅仅读)。在const修饰的常量编译期间,就已经确定下来了。 |
4 const和#define的差别
对照加深 C++中的const常量类似于宏定义 const int c = 5; ≈ #define c 5 C++中的const常量与宏定义不同 const常量是由编译器处理的,提供类型检查和作用域检查 宏定义由预处理器处理,单纯的文本替换 |
//在func1定义a,在func2中能使用吗? //在func1中定义的b,在func2中能使用吗? |
练习 void fun1() { #define a 10 const int b = 20; //#undef a # undef } void fun2() { printf("a = %d\n", a); //printf("b = %d\n", b); } int main() { fun1(); fun2(); return 0; } |
5 结论
C语言中的const变量
C语言中const变量是仅仅读变量,有自己的存储空间
C++中的const常量
可能分配存储空间,也可能不分配存储空间
当const常量为全局。而且须要在其他文件里使用,会分配存储空间
当使用&操作符。取const常量的地址时。会分配存储空间
当const int &a = 10; const修饰引用时。也会分配存储空间
6引用专题讲座
1引用(普通引用)
变量名回想
变量名实质上是一段连续存储空间的别名。是一个标号(门牌号)
程序中通过变量来申请并命名内存空间
通过变量的名字能够使用存储空间
问题1:对一段连续的内存空间仅仅能取一个别名吗?
1 引用概念
a) 在C++中新添加了引用的概念
b) 引用能够看作一个已定义变量的别名
c) 引用的语法:Type& name = var;
d) 引用做函数參数那?(引用作为函数參数声明时不进行初始化)
void main01() { int a = 10; //c编译器分配4个字节内存。。。a内存空间的别名 int &b = a; //b就是a的别名。 。。 a =11; //直接赋值 { int *p = &a; *p = 12; printf("a %d \n",a); } b = 14; printf("a:%d b:%d", a, b); system("pause"); } |
2 引用是C++的概念
属于C++编译器对C的扩展
问题:C中能够编译通过吗? int main() { int a = 0; int &b = a; //int * const b = &a b = 11; //*b = 11; return 0; } |
结论:请不要用C的语法考虑 b=11 |
3 引用做函数參数
普通引用在声明时必须用其他的变量进行初始化, 引用作为函数參数声明时不进行初始化 |
//05复杂数据类型 的引用 struct Teacher { char name[64]; int age ; }; void printfT(Teacher *pT) { cout<<pT->age<<endl; } //pT是t1的别名 ,相当于改动了t1 void printfT2(Teacher &pT) { //cout<<pT.age<<endl; pT.age = 33; } //pT和t1的是两个不同的变量 void printfT3(Teacher pT) { cout<<pT.age<<endl; pT.age = 45; //仅仅会改动pT变量 ,不会改动t1变量 } void main() { Teacher t1; t1.age = 35; printfT(&t1); printfT2(t1); //pT是t1的别名 printf("t1.age:%d \n", t1.age); //33 printfT3(t1) ;// pT是形參 ,t1 copy一份数据 给pT //---> pT = t1 printf("t1.age:%d \n", t1.age); //35 cout<<"hello..."<<endl; system("pause"); return ; } |
4 引用的意义
1)引用作为其他变量的别名而存在,因此在一些场合能够取代指针 2)引用相对于指针来说具有更好的可读性和有用性 |
5 引用本质思考
思考1:C++编译器背后做了什么工作?
int main() { int a = 10; int &b = a; //b是a的别名。请问c++编译器后面做了什么工作? b = 11; cout<<"b--->"<<a<<endl; printf("a:%d\n", a); printf("b:%d\n", b); printf("&a:%d\n", &a); printf("&b:%d\n", &b); //请思考:对同一内存空间能够取好几个名字吗? system("pause"); return 0; } |
单独定义的引用时,必须初始化。说明非常像一个常量 |
思考2:普通引用有自己的空间吗?
struct Teacer { int &a; int &b; }; int main() { printf("sizeof(Teacher) %d\n",sizeof(Teacer)); system("pause"); return 0; } |
引用是一个有地址。引用是常量。。。。。 char *const p |
6 引用的本质
1)引用在C++中的内部实现是一个常指针
Type& name ?èType*const name
2)C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针同样。
3)从使用的角度。引用会让人误会其仅仅是一个别名,没有自己的存储空间。这是C++为了有用性而做出的细节隐藏
Int main()
{
int x = 10;
func(x);
}
4) 请细致对照间接赋值成立的三个条件
1定义两个变量(一个实參一个形參)
2建立关联实參取地址传给形參
3*p形參去间接的改动实參的值
7引用结论
1)引用在实现上。仅仅只是是把:间接赋值成立的三个条件的后两步和二为一
//当实參传给形參引用的时候,仅仅只是是c++编译器帮我们程序猿手工取了一个实參地址,传给了形參引用(常量指针)
2)当我们使用引用语法的时,我们不去关心编译器引用是怎么做的
当我们分析奇怪的语法现象的时,我们才去考虑c++编译器是怎么做的
8函数返回值是引用(引用当左值)
C++引用使用时的难点:
当函数返回值为引用时
若返回栈变量
不能成为其他引用的初始值
不能作为左值使用
若返回静态变量或全局变量
能够成为其它引用的初始值
就可以作为右值使用,也可作为左值使用
C++链式编程中。经经常使用到引用,运算符重载专题
返回值是基础类型。当引用
int getAA1() { int a; a = 10; return a; } //基础类型a返回的时候。也会有一个副本 int& getAA2() { int a; a = 10; return a; } int* getAA3() { int a; a = 10; return &a; } |
返回值是static变量。当引用
//static修饰变量的时候,变量是一个状态变量 int j() { static int a = 10; a ++; printf("a:%d \n", a); return a; } int& j1() { static int a = 10; a ++; printf("a:%d \n", a); return a; } int *j2() { static int a = 10; a ++; printf("a:%d \n", a); return &a; } void main22() { // j()的运算结果是一个数值,没有内存地址。不能当左值。 。 。 。。 //11 = 100; //*(a>b?&a:&b) = 111; //当被调用的函数当左值的时候。必须返回一个引用。。。 。 。 j1() = 100; //编译器帮我们打造了环境 j1(); *(j2()) = 200; //相当于我们程序猿手工的打造 做左值的条件 j2(); system("pause"); } |
返回值是形參,当引用
int g1(int *p) { *p = 100; return *p; } int& g2(int *p) // { *p = 100; return *p; } //当我们使用引用语法的时候 ,我们不去关心编译器引用是怎么做的 //当我们分析乱码这样的现象的时候,我们才去考虑c++编译器是怎么做的。 。 。。 void main23() { int a1 = 10; a1 = g2(&a1); int &a2 = g2(&a1); //用引用去接受函数的返回值,是不是乱码,关键是看返回的内存空间是不是被编译器回收了。。。。 printf("a1:%d \n", a1); printf("a2:%d \n", a2); system("pause"); } |
返回值非基础类型
struct Teachar
{
charname[64];
intage;
};
//假设返回引用不是基础类型,是一个类。那么情况很赋值。。涉及到copy构造函数和=操作重载,抛砖。。。。
struct Teachar { char name[64]; int age; }; //假设返回引用不是基础类型,是一个类,那么情况很赋值。。涉及到copy构造函数和=操作重载,抛砖。。。。 struct Teachar & OpTeacher(struct Teachar &t1) { } |
9指针引用
#include "iostream" using namespace std; struct Teacher { char name[64]; int age; }; int getTe(Teacher **myp ) { Teacher *p = (Teacher *)malloc(sizeof(Teacher)); if (p ==NULL) { return -1; } memset(p, 0, sizeof(Teacher)); p->age = 33; *myp = p; // return 0; } //指针的引用而已 int getTe2(Teacher* &myp) { myp = (Teacher *)malloc(sizeof(Teacher)); myp->age = 34; return 0; } void main333() { Teacher *p = NULL; //getTe(&p); getTe2(p); printf("age:%d \n", p->age); system("pause"); } |
2常引用
以下開始进入const引用难点
1 使用变量初始化const引用
思考cost int &a = b PK const int &a = 10; ????问题:const引用, |
在C++中能够声明const引用 const Type& name = var; const引用让变量拥有仅仅读属性 |
案例1: int main() { int a = 10; const //int *p = (int *)&b; b = 11; //err //*p = 11; //仅仅能用指针来改变了 cout<<"b--->"<<a<<endl; printf("a:%d\n", a); printf("b:%d\n", b); printf("&a:%d\n", &a); printf("&b:%d\n", &b); system("pause"); return 0; } |
案例2: void main41() { int a = 10; const int &b = a; //const引用使用变量a初始化 a = 11; //b = 12; //通过引用改动a,对不起改动不了 system("pause"); } struct Teacher1 { char name[64]; int age; }; void printTe2(const Teacher1 *const pt) { } //const引用让变量(所指内存空间)拥有仅仅读属性 void printTe(const Teacher1 &t) { //t.age = 11; } void main42() { Teacher1 t1; t1.age = 33; printTe(t1); system("pause"); } |
2使用字面量常量初始化const引用
思考: 1、用变量对const引用初始化。const引用分配内存空间了吗? 2、用常量对const引用初始化,const引用分配内存空间了吗? |
void main() { const int b = 10; printf("b:%d", &b); //int &a1 = 19; 假设不加const编译失败 const int &a = 19; printf("&a:%d \n", &a); system("pause"); } |
3 综合案例
void main() { //普通引用 int a = 10; int &b = a; //常量引用:让变量引用仅仅读属性 const int &c = a; //常量引用初始化分为两种 //1 用变量 初始化 常量引用 { int x = 20; const int& y = x; printf("y:%d \n", y); } //2 用常量 初始化 常量引用 { //int &m = 10; //引用是内存空间的别名 字面量10没有内存空间 没有方法做引用 const int &m = 10; } cout<<"hello..."<<endl; system("pause"); return ; } |
3 const引用结论
1)Const &int e 相当于 const int * conste
2)普通引用 相当于 int *const e1
3)当使用常量(字面量)对const引用进行初始化时。C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
4)使用字面量对const引用初始化后,将生成一个仅仅读变量
4const修饰类
兴许课程介绍
5综合练习
int& j() { static int a = 0; return a; } int& g() { int a = 0; return a; } int main() { int a = g(); int& b = g(); j() = 10; printf("a = %d\n", a); printf("b = %d\n", b); printf("f() = %d\n", f()); system("pause"); return 0; } |
7C++对C的函数扩展
1 inline内联函数
C++中的const常量能够替代宏常数定义,如: const int A = 3; #define A 3 C++中是否有解决方式替代宏代码片段呢?(替代宏代码片段就能够避免宏的副作用! ) |
C++中推荐使用内联函数替代宏代码片段 C++中使用inlinekeyword声明内联函数 |
内联函数声明时inlinekeyword必须和函数定义结合在一起,否则编译器会直接忽略内联请求。 //宏替换和函数调用差别 |
#include "iostream" using namespace std; #define MYFUNC(a, b) ((a) < (b) ? (a) : (b)) inline int myfunc(int a, int b) { return a < b ? a : b; } int main() { int a = 1; int b = 3; //int c = myfunc(++a, b); //头疼系统 int c = MYFUNC(++a, b); printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); system("pause"); return 0; } |
说明1: 必须inline int myfunc(int a, int b)和函数体的实现,写在一块 |
说明2 |
C++编译器能够将一个函数进行内联编译 被C++编译器内联编译的函数叫做内联函数 内联函数在终于生成的代码中是未定义的 C++编译器直接将函数体插入在函数调用的地方 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回) |
说明3:C++编译器不一定准许函数的内联请求。 说明4 内联函数是一种特殊的函数。具有普通函数的特征(參数检查,返回类型等) 内联函数是对编译器的一种请求。因此编译器可能拒绝这样的请求 内联函数由 编译器处理。直接将编译后的函数体插入调用的地方 宏代码片段 由预处理器处理。 进行简单的文本替换,没有不论什么编译过程 |
说明5: 现代C++编译器可以进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译 另外,一些现代C++编译器提供了扩展语法,可以对函数进行强制内联 如:g++中的__attribute__((always_inline))属性 |
说明6: C++中内联编译的限制: 不能存在不论什么形式的循环语句 不能存在过多的条件推断语句 函数体不能过于庞大 不能对函数进行取址操作 函数内联声明必须在调用语句之前 |
编译器对于内联函数的限制并非绝对的。内联函数相对于普通函数的优势仅仅是省去了函数调用时压栈,跳转和返回的开销。 因此。当函数体的运行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。 |
结论: 1)内联函数在编译时直接将函数体插入函数调用的地方 2)inline仅仅是一种请求。编译器不一定同意这样的请求 3)内联函数省去了普通函数调用时压栈,跳转和返回的开销 |
2 默认參数
/*1 C++中能够在函数声明时为參数提供一个默认值。 当函数调用时没有指定这个參数的值,编译器会自己主动用默认值取代 */ |
void myPrint(int x = 3) { printf("x:%d", x); } /*2 函数默认參数的规则 仅仅有參数列表后面部分的參数才干够提供默认參数值 一旦在一个函数调用中開始使用默认參数值。那么这个參数后的全部參数都必须使用默认參数值 */ |
//默认參数 void printAB(int x = 3) { printf("x:%d\n", x); } //在默认參数规则 ,假设默认參数出现。那么右边的都必须有默认參数 void printABC(int a, int b, int x = 3, int y=4, int z = 5) { printf("x:%d\n", x); } int main62(int argc, char *argv[]) { printAB(2); printAB(); system("pause"); return 0; } |
3 函数占位參数
/* 函数占位參数 占位參数仅仅有參数类型声明。而没有參数名声明 普通情况下,在函数体内部无法使用占位參数 */ |
int func(int a, int b, int ) { return a + b; } int main01() { //func(1, 2); //能够吗? printf("func(1, 2, 3) = %d\n", func(1, 2, 3)); getchar(); return 0; } |
4 默认參数和占位參数
/* 能够将占位參数与默认參数结合起来使用 意义 为以后程序的扩展留下线索 兼容C语言程序中可能出现的不规范写法 */ //C++能够声明占位符參数,占位符參数一般用于程序扩展和对C代码的兼容 |
int func2(int a, int b, int = 0) { return a + b; } void main() { //假设默认參数和占位參数在一起,都能调用起来 func2(1, 2); func2(1, 2, 3); system("pause"); } |
结论://假设默认參数和占位參数在一起,都能调用起来 |
5 函数重载(Overroad)
函数重载概念
1 函数重载概念 函数重载(Function Overload) 用同一个函数名定义不同的函数 当函数名和不同的參数搭配时函数的含义不同 2 函数重载的推断标准 /* 函数重载至少满足以下的一个条件: 參数个数不同 參数类型不同 參数顺序不同 */ 3 函数返回值不是函数重载的推断标准 实验1:调用情况分析;实验2:推断标准 |
//两个难点:重载函数和默认函数參数混搭 重载函数和函数指针 /* int func(int x) { return x; } int func(int a, int b) { return a + b; } int func(const char* s) { return strlen(s); } int main() { int c = 0; c = func(1); printf("c = %d\n", c); c = func(1, 2); printf("c = %d\n", c); c = func("12345"); printf("c = %d\n", c); printf("Press enter to continue ..."); getchar(); return 0; } */ |
函数重载的调用准则
/* 编译器调用重载函数的准则 将全部同名函数作为候选者 尝试寻找可行的候选函数 精确匹配实參 通过默认參数可以匹配实參 通过默认类型转换匹配实參 匹配失败 终于寻找到的可行候选函数不唯一,则出现二义性。编译失败。 无法匹配全部候选者,函数没有定义,编译失败。 */ /* 函数重载的注意事项 重载函数在本质上是相互独立的不同函数(静态链编) 重载函数的函数类型是不同的 函数返回值不能作为函数重载的根据 函数重载是由函数名和參数列表决定的。 */ |
函数重载是发生在一个类中里面 |
函数重载遇上函数默认參数
//当函数默认參数遇上函数重载会发生什么 /* int func(int a, int b, int c = 0) { return a * b * c; } int func(int a, int b) { return a + b; } //1个參数的同意吗 int func(int a) { return a + b; } int main() { int c = 0; c = func(1, 2); // 存在二义性。调用失败,编译不能通过 printf("c = %d\n", c); printf("Press enter to continue ..."); getchar(); return 0; } */ |
函数重载和函数指针结合
/* 函数重载与函数指针 当使用重载函数名对函数指针进行赋值时 依据重载规则挑选与函数指针參数列表一致的候选者 严格匹配候选者的函数类型与函数指针的函数类型 */ /* int func(int x) // int(int a) { return x; } int func(int a, int b) { return a + b; } int func(const char* s) { return strlen(s); } typedef int(*PFUNC)(int a); // int(int a) int main() { int c = 0; PFUNC p = func; c = p(1); printf("c = %d\n", c); printf("Press enter to continue ..."); getchar(); return 0; } */ |
函数重载、重写、重定义
兴许课程。
8附录
附录1:C++语言对C语言扩充和增强的几点详细体现
附录2:C语言registerkeyword—最快的keyword
register:这个keyword请求编译器尽可能的将变量存在CPU内部寄存器中。而不是通过内存寻址訪问。以提高效率。注意是尽可能。不是绝对。你想想。一个CPU 的寄存器也就那么几个或几十个,你要是定义了非常多非常多register 变量,它累死也可能不能所有把这些变量放入寄存器吧,轮也可能轮不到你。 一、皇帝身边的小太监----寄存器 不知道什么是寄存器?那见过太监没有?没有?事实上我也没有。没见过不要紧,见过就麻烦大了。^_^,大家都看过古装戏,那些皇帝们要阅读奏章的时候,大臣总是先将奏章交给皇帝旁边的小太监,小太监呢再交给皇帝同志处理。这个小太监仅仅是个中转站。并无别的功能。 好,那我们再联想到我们的CPU。CPU 不就是我们的皇帝同志么?大臣就相当于我们的内存。数据从他这拿出来。 那小太监就是我们的寄存器了(这里先不考虑CPU 的快速缓存区)。 数据从内存里拿出来先放到寄存器,然后CPU 再从寄存器里读取数据来处理,处理完后相同把数据通过寄存器存放到内存里。CPU不直接和内存打交道。这里要说明的一点是:小太监是主动的从大臣手里接过奏章,然后主动的交给皇帝同志。但寄存器没这么自觉,它从不主动干什么事。一个皇帝可能有好些小太监,那么一个CPU 为啥要这么麻烦啊?速度。就是由于速度。 寄存器事实上就是一块一块小的存储空间,仅仅只是其存取速度要比内存快得多。进水楼台先得月嘛。它离CPU 非常近。CPU 一伸手就拿到数据了,比在那么大的一块内存里去寻找某个地址上的数据是不是快多了?那有人问既然它速度那么快,那我们的内存硬盘都改成寄存器得了呗。我要说的是:你真有钱! |
二、举例 register修饰符暗示编译程序对应的变量将被频繁地使用,假设可能的话,应将其保存在CPU的寄存器中。以加快其存储速度。比如以下的内存块拷贝代码。 #ifdef NOSTRUCTASSIGN memcpy (d, s, l) { register char *d; register char *s; register int i; while (i--) *d++ = *s++; } #endif
三、使用register 修饰符的注意点 可是使用register修饰符有几点限制。 首先,register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值。而且长度应该小于或者等于整型的长度。只是。有些机器的寄存器也能存放浮点数。 其次,由于register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。 因为寄存器的数量有限,并且某些寄存器仅仅能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于执行程序的机器,而不论什么多余的register修饰符都将被编译程序所忽略。 在某些情况下,把变量保存在寄存器中反而会减少程序的执行速度。由于被占用的寄存器不能再用于其他目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。 早期的C编译程序不会把变量保存在寄存器中。除非你命令它这样做。这时register修饰符是C语言的一种非常有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,如今的C编译环境能比程序猿做出更好的决定。实际上,很多编译程序都会忽略register修饰符。由于虽然它全然合法。但它只是暗示而不是命令。 |
9 作业及强化训练
1 复杂数据类型引用做函数參数
分析内存四区变化图
2 代码敲一遍
3 设计一个类, 求圆形的周长
4 设计一个学生类,属性有姓名和学号,
能够给姓名和学号赋值
能够显示学生的姓名和学号
2、类和对象
1前言
C++学习技术路线及目标
研究C++编译器管理类和对象的方法===》避免死角
c++编译器对类对象的生命周期管理,对象创建、使用、销毁
c++面向对象模型初探
c++面向对象多态原理探究
操作符重载
C++基础课程学习完成以后,有没有一个标准,来推断自己有没有入门。
面向抽象类(接口)编程
2类和对象
2.1 基本概念
1)类、对象、成员变量、成员函数
2)面向对象三大概念
封装、继承、多态
3)编程实践
类的定义和对象的定义。对象的使用
求圆形的面积
定义Teacher类,打印Teacher的信息(把类的声明和类的实现分开)
2.2类的封装
1)封装(Encapsulation)
A)封装,是面向对象程序设计最主要的特性。把数据(属性)和函数(操作)合成一个总体,这在计算机世界中是用类与对象实现的。
B)封装,把客观事物封装成抽象的类,而且类能够把自己的数据和方法仅仅让可信的类或者对象操作,对不可信的进行信息隐藏。
备注:有2层含义(把属性和方法进行封装对属性和方法进行訪问控制)
C++中类的封装
成员变量。C++中用于表示类属性的变量
成员函数,C++中用于表示类行为的函数
2)类成员的訪问控制
在C++中能够给成员变量和成员函数定义訪问级别
Public修饰成员变量和成员函数能够在类的内部和类的外部被訪问
Private修饰成员变量和成员函数仅仅能在类的内部被訪问
//类是把属性和方法封装 同一时候对信息进行訪问控制 //类的内部。类的外部 //我们抽象了一个类。用类去定义对象 //类是一个数据类型。类是抽象的 //对象是一个详细的变量。。 占用内存空间。 class Circle { public: double r; double s; public: double getR() { a++; return r; } void setR(double val) { r = val; } public: double getS() //添加功能时,是在改动类, 改动类中的属性或者是方法 { s = 3.14f*r*r; return s; } //private: int a; }; |
3)struct和classkeyword差别
在用struct定义类时。全部成员的默认属性为public
在用class定义类时,全部成员的默认属性为private
2.3 C++面向对象程序设计举例
目标:面向过程向面向对象思想转变
刚開始学习的人要细致体会类和对象之间的关系。并通过适当练习巩固和提高!
案例1 设计立方体类(cube),求出立方体的面积和体积
求两个立方体。是否相等(全局函数和成员函数)
案例2 设计一个圆形类(AdvCircle),和一个点类(Point)。计算点在圆内部还是圆外
即:求点和圆的关系(圆内和圆外)
案例3 对于第二个案例,类的声明和类的实现分开
2.4 作业
作业1:编写C++程序完毕下面功能:
1)定义一个Point类。其属性包含点的坐标。提供计算两点之间距离的方法;
2)定义一个圆形类。其属性包含圆心和半径。
3)创建两个圆形对象。提示用户输入圆心坐标和半径,推断两个圆是否相交,并输出结果。
作业2:设计并測试一个名为Rectangle的矩形类,其属性为矩形的左下角与右上角两个点的坐标。依据坐标能计算出矩形的面积
作业3:定义一个Tree类,有成员ages(树龄),成员函数grow(int years)对ages加上years,age()显示tree对象的ages的值。
3对象的构造和析构
前言
创建一个对象时,经常须要作某些初始化的工作,比如对数据成员赋初值。
注意,类的数据成员是不能在声明类时初始化的。
为了解决问题。C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数。与其它成员函数不同,不须要用户来调用它。而是在建立对象时自己主动运行。
3.1构造和析构函数
1构造函数和析构函数的概念
有关构造函数
1构造函数定义及调用
1)C++中的类能够定义与类名同样的特殊成员函数。这样的与类名同样的成员函数叫做构造函数;
2)构造函数在定义时能够有參数;
3)没有不论什么返回类型的声明。
2构造函数的调用
自己主动调用:普通情况下C++编译器会自己主动调用构造函数
手动调用:在一些情况下则须要手工调用构造函数
有关析构函数
3)析构函数定义及调用
1)C++中的类能够定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
语法:~ClassName()
2)析构函数没有參数也没有不论什么返回类型的声明
3)析构函数在对象销毁时自己主动被调用
4)析构函数调用机制
C++编译器自己主动调用
代码演示:dm01_构造函数的基础.cpp
2 C++编译器构造析构方案 PK 对象显示初始化方案
设计构造函数和析构函数的原因
面向对象的思想是从生活中来。手机、车出厂时,是一样的。
生活中存在的对象都是被初始化后才上市的。初始状态是对象普遍存在的一个状态的
普通方案:
为每一个类都提供一个public的initialize函数。
对象创建后马上调用initialize函数进行初始化。
优缺点分析
1)initialize仅仅是一个普通的函数,必须显示的调用
2)一旦因为失误的原因,对象没有初始化。那么结果将是不确定的
没有初始化的对象,其内部成员变量的值是不定的
3)不能全然解决这个问题
//为什么对象须要初始化 有什么样的初始化方案 #include "iostream" using namespace std; /* 思考为什么须要初始化 面向对象思想来自生活,手机、车、电子产品,出厂时有初始化 怎么样进行初始化? 方案1:显示调用方法 缺点:易忘、麻烦。显示调用init,不能全然解决这个问题 */ class Test21 { public: int m; int getM() const { return m; } void setM(int val) { m = val; } int n; int getN() const { return n; } void setN(int val) { n = val; } public: int init(int m,int n) { this->m = m; this->n = n; return 0; } protected: private: }; int main() { int rv =0; Test21 t1; //无參构造函数的调用方法 Test21 t2; //t1.init(100, 200); //t2.init(300, 400); cout<<t1.getM()<<" "<<t1.getN()<<endl; cout<<t2.getM()<<" "<<t2.getN()<<endl; //定义对象数组时,没有机会进行显示初始化 Test21 arr[3]; //Test arr_2[3] = {Test(1,3), Test(), Test()}; system("pause"); return rv; } |
3.2构造函数的分类及调用
C++编译器给程序猿提供的对象初始化方案,高端大气上档次。
//有參数构造函数的三种调用方法 class Test { private: int a; int b; public: //无參数构造函数 Test() { ; } //带參数的构造函数 Test(int a, int b) { ; } //赋值构造函数 Test(const Test &obj) { ; } public: void init(int _a, int _b) { a = _a; b = _b; } }; |
1无參数构造函数
调用方法: Testt1, t2;
2有參构造函数
有參构造函数的三种调用方法
//有參数构造函数的三种调用方法 class Test5 { private: int a; public: //带參数的构造函数 Test5(int a) { printf("\na:%d", a); } Test5(int a, int b) { printf("\na:%d b:%d", a, b); } public: }; int main55() { Test5 t1(10); //c++编译器默认调用有參构造函数 括号法 Test5 t2 = (20, 10); //c++编译器默认调用有參构造函数 等号法 Test5 t3 = Test5(30); //程序猿手工调用构造函数 产生了一个对象 直接调用构造构造函数法 system("pause"); return 0; } |
3拷贝构造函数调用时机
赋值构造函数的四种调用场景(调用时机)
第1和第2个调用场景
#include "iostream" using namespace std; class AA { public: AA() //无參构造函数 默认构造函数 { cout<<"我是构造函数。自己主动被调用了"<<endl; } AA(int _a) //无參构造函数 默认构造函数 { a = _a; } AA(const AA &obj2) { cout<<"我也是构造函数,我是通过另外一个对象obj2,来初始化我自己"<<endl; a = obj2.a + 10; } ~AA() { cout<<"我是析构函数,自己主动被调用了"<<endl; } void getA() { printf("a:%d \n", a); } protected: private: int a; }; //单独搭建一个舞台 void ObjPlay01() { AA a1; //变量定义 //赋值构造函数的第一个应用场景 //用对象1 初始化 对象2 AA a2 = a1; //定义变量并初始化 //初始化法 a2 = a1; //用a1来=号给a2 编译器给我们提供的浅copy } |
第二个应用场景 //单独搭建一个舞台 void ObjPlay02() { AA a1(10); //变量定义 //赋值构造函数的第一个应用场景 //用对象1 初始化 对象2 AA a2(a1); //定义变量并初始化 //括号法 //a2 = a1; //用a1来=号给a2 编译器给我们提供的浅copy a2.getA(); } //注意:初始化操作 和 等号操作 是两个不同的概念 |
第3个调用场景
#include "iostream" using namespace std; 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 ; } ; //alt + f8 排版 void f ( Location p ) { cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ; } void mainobjplay() { Location A ( 1, 2 ) ; //形參是一个元素,函数调用,会运行实參变量初始化形參变量 f ( A ) ; } void main() { mainobjplay(); system("pause"); } |
第4个调用场景
第四个应用场景 #include "iostream" using namespace std; 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 ; } ; //alt + f8 排版 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)默认拷贝构造函数
当类中未定义拷贝构造函数时。编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制
3.3构造函数调用规则研究
1)当类中未定义不论什么一个构造函数时。c++编译器会提供默认无參构造函数和默认拷贝构造函数
2)当类中定义了拷贝构造函数时,c++编译器不会提供无參数构造函数
3) 当类中定义了随意的非拷贝构造函数(即:当类中提供了有參构造函数或无參构造函数)。c++编译器不会提供默认无參构造函数
4 )默认拷贝构造函数成员变量简单赋值
总结:仅仅要你写了构造函数,那么你必须用。
构造析构阶段性总结
1)构造函数是C++中用于初始化对象状态的特殊函数
2)构造函数在对象创建时自己主动被调用
3)构造函数和普通成员函数都遵循重载规则
4)拷贝构造函数是对象正确初始化的重要保证
5)必要的时候,必须手工编写拷贝构造函数
========》1个对象的初始化讲完了,添加一个案例。
3.4深拷贝和浅拷贝
? 默认复制构造函数能够完毕对象的数据成员值简单的复制
? 对象的数据资源是由指针指示的堆时,默认复制构造函数仅作指针值复制
1浅拷贝问题抛出和分析
深拷贝浅拷贝现象出现的原因
2浅拷贝程序C++提供的解决方法
显示提供copy构造函数
显示操作重载=号操作,不使用编译器提供的浅copy
class Name { public: Name(const char *pname) { 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 operator=(Name &obj3) { 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 main61() { playObj(); system("pause"); } |
3.5多个对象构造和析构
1对象初始化列表
1)对象初始化列表出现原因
1.必须这样做:
假设我们有一个类成员,它本身是一个类或者是一个结构,并且这个成员它仅仅有一个带參数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带參数的构造函数,
假设没有初始化列表。那么他将无法完毕第一步,就会报错。
2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值
当类成员中含有一个const对象时。或者是一个引用时。他们也必需要通过成员初始化列表进行初始化。
由于这两种对象要在声明后立即初始化,而在构造函数中,做的是对他们的赋值,这样是不被同意的。
2)C++中提供初始化列表对成员变量进行初始化
语法规则
Constructor::Contructor() : m1(v1),m2(v1,v2), m3(v3)
{
// some other assignment operation
}
3)注意概念
初始化:被初始化的对象正在创建
赋值:被赋值的对象已经存在
4)注意:
成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关
初始化列表先于构造函数的函数体运行
/* 1 C++中提供了初始化列表对成员变量进行初始化 2 使用初始化列表出现原因: 1.必须这样做: 假设我们有一个类成员。它本身是一个类或者是一个结构,并且这个成员它仅仅有一个带參数的构造函数, 而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带參数的构造函数, 假设没有初始化列表,那么他将无法完毕第一步,就会报错。 2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值 当类成员中含有一个const对象时,或者是一个引用时,他们也必需要通过成员初始化列表进行初始化, 由于这两种对象要在声明后立即初始化。而在构造函数中,做的是对他们的赋值,这样是不被同意的。 */ //总结 构造和析构的调用顺序 #include "iostream" using namespace std; 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 m; }; int run() { MyD myD; return 0; } int main_dem03() { run(); system("pause"); return 0; } |
3.6构造函数和析构函数的调用顺序研究
构造函数与析构函数的调用顺序
1)当类中有成员变量是其他类的对象时。首先调用成员变量的构造函数,调用顺序与声明顺序同样;之后调用自身类的构造函数
2)析构函数的调用顺序与相应的构造函数调用顺序相反
3.7构造函数和析构函数综合练习
通过训练,把所学知识点都穿起来
1构造析综合训练
demo10_构造析构练习强化.cpp (解说)
展示分析过程。注意赋值构函数的调用
2匿名对象强化训练
demo10_构造析构练习强化.cpp
1) 匿名对象生命周期
2) 匿名对象的去和留
3匿名对象强化训练
3) 构造中调用构造
demo11_匿名对象练习强化.cpp
构造函数中调用构造函数,是一个蹩脚的行为。
3.8 对象的动态建立和释放
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。用户能够依据该指针的值推断分配空间是否成功。
4) 应用举例
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 mallocfree函数 ckeyword
// newdelete 操作符号 c++的keyword
//2 new 在堆上分配内存 delete
//分配基础类型 、分配数组类型、分配对象
//3 new和malloc 深入分析
混用測试、异同比較
结论: malloc不会调用类的构造函数
Free不会调用类的析构函数
4静态成员变量成员函数
思考:每一个变量。拥有属性。
有没有一些属性,归全部对象拥有?
4.1静态成员变量
1)定义静态成员变量
? keyword 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)使用静态成员变量
// 例5-14 使用公有静态数据成员
#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;
}
4.2静态成员函数
1)概念
? 静态成员函数数冠以keywordstatic
? 静态成员函数提供不依赖于类数据结构的共同操作。它没有this指针
? 在类外调用静态成员函数用“类名 :: ”作限定词。或通过对象调用
2)案例
3)疑难问题:静态成员函数中,不能使用普通变量。
//静态成员变量属于整个类的,分不清楚,是那个详细对象的属性。
4.3综合训练
5 C++面向对象模型初探
前言
C++对象模型能够概括为下面2部分:
1. 语言中直接支持面向对象程序设计的部分,主要涉及如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等。
2. 对于各种支持的底层实现机制。
在c语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说。语言本身并没有支持“数据和函数”之间的关联性。
在c++中,通过抽象数据类型(abstractdata type,ADT)。在类中定义数据和函数,来实现数据和函数直接的绑定。
概括来说,在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。
5.1基础知识
C++中的class从面向对象理论出发,将变量(属性)和函数(方法)集中定义在一起,用于描写叙述现实世界中的类。从计算机的角度。程序依旧由数据段和代码段构成。 C++编译器怎样完毕面向对象理论到计算机程序的转化? 换句话:C++编译器是怎样管理类、对象、类和对象之间的关系 详细的说:详细对象调用类中的方法,那,c++编译器是怎样区分,是那个详细的类,调用这种方法那? |
思考一下程序结果 |
#include "iostream" using namespace std; class C1 { public: int i; //4 int j; //4 int k; //4 protected: private: }; //12 class C2 { public: int i; //4 int j; //4 int k; //4 static int m; //4 public: int getK() const { return k; } //4 void setK(int val) { k = val; } //4 protected: private: }; //12 16 24 struct S1 { int i; int j; int k; }; // struct S2 { int i; int j; int k; static int m; }; // 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"); } |
5.2编译器对属性和方法的处理机制
通过上面的案例,我们能够的得出: 1)C++类对象中的成员变量和成员函数是分开存储的 成员变量: 普通成员变量:存储于对象中。与struct变量有同样的内存布局和字节对齐方式 静态成员变量:存储于全局数据区中 成员函数:存储于代码段中。 问题出来了:非常多对象共用一块代码?代码是怎样区分详细对象的那? 换句话说:int getK() const { return k; },代码是怎样区分。详细obj1、obj2、obj3对象的k值? |
2)C++编译器对普通成员函数的内部处理 |
请细致思考。并说出你的总结! |
5.3总结
1、C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效。 2、C++中类的普通成员函数都隐式包括一个指向当前对象的this指针。 3、静态成员函数、成员变量属于类 静态成员函数与普通成员函数的差别 静态成员函数不包括指向详细对象的指针 普通成员函数包括一个指向详细对象的指针 |
5.4 this指针
实验1:若类成员函数的形參和 类的属性。名字同样,通过this指针来解决。
实验2:类的成员函数可通过const修饰,请问const修饰的是谁
5.5全局函数PK成员函数
1、把全局函数转化成成员函数,通过this指针隐藏左操作数
Testadd(Test &t1, Test &t2)===》Test add( Test&t2)
2、把成员函数转换成全局函数,多了一个參数
voidprintAB()===》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;
}
voidadd3(Test &t2) //*this //函数返回元素
{
//t3是局部变量
Test t3(this->a+t2.getA(),this->b + t2.getB()) ;
//return t3;
}
6友元
6.1友元函数
比如
class A1 { public: A1() { a1 = 100; a2 = 200; } int getA1() { return this->a1; } //声明一个友元函数 friend void setA1(A1 *p, int a1); //这个函数是这个类的好朋友 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"); } |
6.2友元类
? 若B类是A类的友员类。则B类的全部成员函数都是A类的友员函数
? 友员类通常设计为一种对数据操作或类之间传递消息的辅助类
7强化训练
1 statickeyword强化训练题
? 某商店经销一种货物。货物购进和卖出时以箱为单位。各箱的重量不一样,因此。商店须要记录眼下库存的总重量。如今用C++模拟商店货物购进和卖出的情况。
#include "iostream" using namespace std; class Goods { public : Goods ( int w) { weight = w ; total_weight += w ; } ~ Goods() { total_weight -= weight ; } int Weight() { return weight ; } ; static int TotalWeight() { return total_weight ; } Goods *next ; private : int weight ; static int total_weight ; } ; 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 ) ; } |
2 数组类封装
目标:解决实际问题,训练构造函数、copy构造函数等,为操作符重载做准备
数组类的測试
#include "iostream" #include "Array.h" using namespace std; int main() { Array a1(10); for(int i=0; i<a1.length(); i++) { a1.setData(i, i); } for(int i=0; i<a1.length(); i++) { printf("array %d: %d\n", i, a1.getData(i)); } Array a2 = a1; for(int i=0; i<a2.length(); i++) { printf("array %d: %d\n", i, a2.getData(i)); } system("pause"); return 0; } |
数组类的头文件
#ifndef _MYARRAY_H_ #define _MYARRAY_H_ class Array { private: int mLength; int* mSpace; public: Array(int length); Array(const Array& obj); int length(); void setData(int index, int value); int getData(int index); ~Array(); }; #endif |
3小结
? 类通经常使用keywordclass定义。
类是数据成员和成员函数的封装。
类的实例称为对象。
? 结构类型用keywordstruct定义,是由不同类型数据组成的数据类型。
? 类成员由private, protected, public决定訪问特性。
public成员集称为接口。
? 构造函数在创建和初始化对象时自己主动调用。
析构函数则在对象作用域结束时自己主动调用。
? 重载构造函数和复制构造函数提供了创建对象的不同初始化方式。
? 静态成员是局部于类的成员,提供一种同类对象的共享机制。
? 友员用keywordfriend声明。友员是对类操作的一种辅助手段。一个类的友员能够訪问该类各种性质的成员。
? 链表是一种重要的动态数据结构,能够在程序执行时创建或撤消数据元素。
8运算符重载
8.1概念
什么是运算符重载
所谓重载,就是又一次赋予新的含义。
函数重载就是对一个已有的函数赋予新的含义,使之实现新功能。因此,一个函数名就能够用来代表不同功能的函数,也就是”一名多用”。
运算符也能够重载。实际上,我们已经在不知不觉之中使用了运算符重载。比如,大家都已习惯于用加法运算符”+”对整数、单精度数和双精度数进行加法运算,如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 operator+(Complex &c1, Complex &c2) { 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 ; } |
8.2运算符重载的限制
8.3运算符重载编程基础
比如:
//全局函数 完毕 +操作符 重载
Complex operator+(Complex &c1, Complex&c2)
//类成员函数 完毕 -操作符 重载
Complex operator-(Complex &c2)
运算符重载的两种方法
比如1:
//通过类成员函数完毕-操作符重载
//函数声明 Complexoperator-(Complex &c2)
//函数调用分析
//用类成员函数实现-运算符重载
Complex c4 = c1 - c2;
c4.printCom();
//c1.operator-(c2);
比如2:
//通过全局函数方法完毕+操作符重载
//函数声明 Complexoperator+(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: 学员自己练习 实现 * /
比如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 后置— 操作符 用类成员函数实现
Complexoperator--(int)
{
Complextmp = *this;
this->a--;
this->b--;
returntmp;
}
//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);
3) 友元函数重载操作符使用注意点
a) 友员函数重载运算符经常使用于运算符的左右操作数类型不同的情况
b)其它
? 在第一个參数须要隐式转换的情形下,使用友员函数重载运算符是正确的选择
? 友员函数没有 this 指针,所需操作数都必须在參数表显式声明,非常easy实现类型的隐式转换
? C++中不能用友员函数重载的运算符有
= () [] ->
4 )友元函数案例vector类
#include <iostream> using namespace std; //为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 int[ size ] ; len = size ; } 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"); } |
8.4运算符重载提高
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 char [m_len+1]; //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 <cstdlib> #include <iostream> using namespace std; 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 operator&& (const Test& obj) { 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.operator&&( t1 + t2) ) T1.operator&&( t1.operator+(t2) ) //t1 && t1.operator+(t2) // t1.operator( t1.operator(t2) ) cout<<"两个函数都被运行了,并且是先运行了+"<<endl; } system("pause"); return ; } |
8.5运算符重载在项目开发中的应用
1实现一个数组类
加入<< >>
2实现一个字符串类
构造函数要求
//C语言中 没有字符串这样的类型。是通过数组来模拟字符串
//C++中 我们来设计一个字符串类 以零结尾的字符串
//若len为0,表示空串
MyString a; //空串“”
MyString a(“dddd”);
MyString b = a;
b = “aaaaaa”
b = a;
if (a > b)
if (a == b)
b[i] = ‘a’;
经常使用的操作符
<< >> != == > < =
//C语言中 没有字符串这样的类型,是通过数组来模拟字符串 //C++中 我们来设计一个字符串 以零结尾的字符串 class MyString { friend ostream& operator<<(ostream &out, const MyString &s); public: //构造和析构 MyString(int len = 0); MyString(const char *p); MyString(const MyString& obj); ~MyString(); public: //操作符重载 MyString& operator=(const char *p); MyString& operator=(const MyString& obj); char& operator[](int index) const; public: bool operator==(const char* p) const; bool operator!=(const char* p) const; bool operator==(const MyString& s) const; bool operator!=(const MyString& s) const; public: //string to c char *c_str(); const char* c_str() const; int length() { return m_len; } public: int operator<(const char *p); int operator>(const char *p); int operator<(const MyString &s); int operator>(const MyString &s); private: int m_len; char *m_p; }; |
3智能指针类编写
1问题抛出
指针使用过程中,常常会出现内存泄漏和内存多次被释放常
2 解决方式:比如:boost库的智能指针
项目开发中,要求开发人员使用预先编写的智能指针类对象取代C语言中的原生指针
3 智能指针思想
project中的智能指针是一个类模板
通过构造函数接管申请的内存
通过析构函数确保堆内存被及时释放
通过重载指针运算符* 和 -> 来模拟指针的行为
通过重载比較运算符 == 和 != 来模拟指针的比較
class Test { public: Test() { this->a = 10; } void printT() { cout<<a<<endl; } private: int a; }; class MyTestPointer { public: public: MyTestPointer() { p = NULL; } MyTestPointer(Test* p) { this->p = p; } ~MyTestPointer() { delete p; } Test* operator->() { return p; } Test& operator*() { return *p; } protected: Test *p; }; void main01_classp() { Test *p = new Test; p->printT(); delete p; MyTestPointer myp = new Test; //构造函数 myp->printT(); //重载操作符 -> }; |
class MyIntPointer { public: public: MyIntPointer() { p = NULL; } MyIntPointer(int* p) { this->p = p; } ~MyIntPointer() { delete p; } int* operator->() { return p; } int& operator*() { return *p; } protected: int *p; }; void main02_intp() { int *p = new int(100); cout<<*p<<endl; delete p; MyIntPointer myp = new int(200); cout<<*myp<<endl; //重载*操作符 }; |
8.7附录:运算符和结合性
总结
操作符重载是C++的强大特性之中的一个
操作符重载的本质是通过函数扩展操作符的语义
operatorkeyword是操作符重载的关键
friendkeyword能够对函数或类开发訪问权限
操作符重载遵循函数重载的规则
操作符重载能够直接使用类的成员函数实现
=, [], ()和->操作符仅仅能通过成员函数进行重载
++操作符通过一个int參数进行前置与后置的重载
C++中不要重载&&和||操作符
3、继承和派生
3.1继承概念
面向对象程序设计有4个主要特点:抽象、封装、继承和多态性。我们已经解说了类和对象,了解了面向对象程序设计的两个重要特征一数据抽象与封装,已经可以设计出基于对象的程序。这是面向对象程序设计的基础。
要较好地进行面向对象程序设计。还必须了解面向对象程序设计另外两个重要特 征——继承性和多态性。本章主要介绍有关继承的知识。多态性将在兴许章节中解说。
继承性是面向对象程序设计最重要的特征。能够说,假设没有掌握继承性,就等于没有掌握类和对象的精华。就是没有掌握面向对象程序设计的真谛。
3.1.1类之间的关系
has-A,uses-A 和 is-A
has-A 包括关系。用以描写叙述一个类由多个“部件类”构成。
实现has-A关系用类成员表示,即一个类中的数据成员是还有一种已经定义的类。
uses-A 一个类部分地使用还有一个类。
通过类之间成员函数的相互联系,定义友员或对象參数传递实现。
is-A 机制称为“继承”。关系具有传递性,不具有对称性。
3.1.2继承关系举例
万事万物中皆有继承,是重要的现象
两个案例:1)植物继承图;2)程序猿继承图
3.1.3 继承相关概念
3.1.4 派生类的定义
注意:C++中的继承方式(public、private、protected)会影响子类的对外訪问属性。
3.1.5 继承重要说明
1、子类拥有父类的全部成员变量和成员函数
2、子类能够拥有父类没有的方法和属性
3、子类就是一种特殊的父类
4、子类对象能够当作父类对象使用
3.2派生类的訪问控制
派生类继承了基类的所有成员变量和成员方法(除了构造和析构之外的成员方法),可是这些成员的訪问属性,在派生过程中是能够调整的。
3.2.1单个类的訪问控制
1、类成员訪问级别(public、private、protected)
2、思考:类成员的訪问级别仅仅有public和private是否足够?
3.2.2不同的继承方式会改变继承成员的訪问属性
1)C++中的继承方式会影响子类的对外訪问属性
public继承:父类成员在子类中保持原有訪问级别
private继承:父类成员在子类中变为private成员
protected继承:父类中public成员会变成protected
父类中protected成员仍然为protected
父类中private成员仍然为private
2)private成员在子类中依旧存在,可是却无法訪问到。不论种方式继承基类,派生类都不能直接使用基类的私有成员。
3)C++中子类对外訪问属性表
父类成员訪问级别 |
||||
继 承 方 式 |
public |
proteced |
private |
|
public |
public |
proteced |
private |
|
proteced |
proteced |
proteced |
private |
|
private |
private |
private |
Private |
4)继承中的訪问控制
3.2.3“三看”原则
C++中的继承方式(public、private、protected)会影响子类的对外訪问属性
推断某一句话,是否能被訪问
1)看调用语句。这句话写在子类的内部、外部
2)看子类怎样从父类继承(public、private、protected)
3)看父类中的訪问级别(public、private、protected)
3.2.3派生类类成员訪问级别设置的原则
思考:怎样恰当的使用public,protected和private为成员声明訪问级别?
1、须要被外界訪问的成员直接设置为public
2、仅仅能在当前类中訪问的成员设置为private
3、仅仅能在当前类和子类中訪问的成员设置为protected,protected成员的訪问权限介于public和private之间。
3.2.4综合训练
练习:
public继承不会改变父类对外訪问属性。
private继承会改变父类对外訪问属性为private;
protected继承会部分改变父类对外訪问属性。
结论:普通情况下class B : public A
//类的继承方式对子类对外訪问属性影响 #include <cstdlib> #include <iostream> using namespace std; class A { private: int a; protected: int b; public: int c; A() { a = 0; b = 0; c = 0; } void set(int a, int b, int c) { this->a = a; this->b = b; this->c = c; } }; class B : public A { public: void print() { //cout<<"a = "<<a; //err cout<<"b = "<<b; cout<<"c = "<<endl; } }; class C : protected A { public: void print() { //cout<<"a = "<<a; //err cout<<"b = "<<b; cout<<"c = "<<endl; } }; class D : private A { public: void print() { //cout<<"a = "<<a; //err cout<<"b = "<<b<<endl; cout<<"c = "<<c<<endl; } }; int main_01(int argc, char *argv[]) { A aa; B bb; C cc; D dd; aa.c = 100; //ok bb.c = 100; //ok //cc.c = 100; //err 类的外部是什么含义 //dd.c = 100; //err aa.set(1, 2, 3); bb.set(10, 20, 30); //cc.set(40, 50, 60); //ee //dd.set(70, 80, 90); //ee bb.print(); cc.print(); dd.print(); system("pause"); return 0; } |
3.3继承中的构造和析构
3.3.1类型兼容性原则
类型兼容规则是指在须要基类对象的不论什么地方。都能够使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的全部成员。这样。公有派生类实际就具备了基类的全部功能。凡是基类能解决的问题。公有派生类都能够解决。类型兼容规则中所指的替代包含下面情况:
子类对象能够当作父类对象使用
子类对象能够直接赋值给父类对象
子类对象能够直接初始化父类对象
父类指针能够直接指向子类对象
父类引用能够直接引用子类对象
在替代之后,派生类对象就能够作为基类的对象使用。可是仅仅能使用从基类继承的成员。
类型兼容规则是多态性的重要基础之中的一个。
总结:子类就是特殊的父类 (base *p = &child;)
#include <cstdlib> #include <iostream> using namespace std; /* 子类对象能够当作父类对象使用 子类对象能够直接赋值给父类对象 子类对象能够直接初始化父类对象 父类指针能够直接指向子类对象 父类引用能够直接引用子类对象 */ //子类就是特殊的父类 class Parent03 { protected: const char* name; public: Parent03() { name = "Parent03"; } void print() { cout<<"Name: "<<name<<endl; } }; class Child03 : public Parent03 { protected: int i; public: Child03(int i) { this->name = "Child2"; this->i = i; } }; int main() { Child03 child03(1000); //分别定义父类对象 父类指针 父类引用 child Parent03 parent = child03; Parent03* pp = &child03; Parent03& rp = child03; parent.print(); pp->print(); rp.print(); system("pause"); return 0; } |
3.3.2继承中的对象模型
类在C++编译器的内部能够理解为结构体
子类是由父类成员叠加子类新成员得到的
继承中构造和析构
问题:怎样初始化父类成员?父类与子类的构造函数有什么关系
在子类对象构造时,须要调用父类构造函数对其继承得来的成员进行初始化
在子类对象析构时。须要调用父类析构函数对其继承得来的成员进行清理
#include <cstdlib> #include <iostream> using namespace std; class Parent04 { public: Parent04(const char* s) { cout<<"Parent04()"<<" "<<s<<endl; } ~Parent04() { cout<<"~Parent04()"<<endl; } }; class Child04 : public Parent04 { public: Child04() : Parent04("Parameter from Child!") { cout<<"Child04()"<<endl; } ~Child04() { cout<<"~Child04()"<<endl; } }; void run04() { Child04 child; } int main_04(int argc, char *argv[]) { run04(); system("pause"); return 0; } |
3.3.3继承中的构造析构调用原则
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数运行结束后。运行子类的构造函数
3、当父类的构造函数有參数时,须要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反
3.3.4继承与组合混搭情况下,构造和析构调用原则
原则: 先构造父类,再构造成员变量、最后构造自己
先析构自己,在析构成员变量、最后析构父类
//先构造的对象,后释放
练习:demo05_extend_construct_destory.cpp
//子类对象怎样初始化父类成员 //继承中的构造和析构 //继承和组合混搭情况下,构造函数、析构函数调用顺序研究 #include <iostream> using namespace std; class Object { public: Object(const char* s) { cout<<"Object()"<<" "<<s<<endl; } ~Object() { cout<<"~Object()"<<endl; } }; class Parent : public Object { public: Parent(const char* s) : Object(s) { cout<<"Parent()"<<" "<<s<<endl; } ~Parent() { cout<<"~Parent()"<<endl; } }; class Child : public Parent { protected: Object o1; Object o2; public: Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!") { cout<<"Child()"<<endl; } ~Child() { cout<<"~Child()"<<endl; } }; void run05() { Child child; } int main05(int argc, char *argv[]) { cout<<"demo05_extend_construct_destory.cpp"<<endl; run05(); system("pause"); return 0; } |
3.3.5继承中的同名成员变量处理方法
1、当子类成员变量与父类成员变量同名时
2、子类依旧从父类继承同名成员
3、在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员。显式地使用类名限定符)
4、同名成员存储在内存中的不同位置
总结:同名成员变量和成员函数通过作用域分辨符进行区分
3.3.6派生类中的statickeyword
继承和statickeyword在一起会产生什么现象哪?
理论知识
? 基类定义的静态成员。将被全部派生类共享
? 依据静态成员自身的訪问特性和派生类的继承方式,在类层次体系中具有不同的訪问性质 (遵守派生类的訪问控制)
? 派生类中訪问静态成员,用下面形式显式说明:
类名 :: 成员
或通过对象訪问 对象名 . 成员
总结:
1> static函数也遵守3个訪问原则
2> static易犯错误(不但要初始化,更重要的显示的告诉编译器分配内存)
3> 构造函数默觉得private
3.4多继承
3.4.1多继承的应用
多继承概念
? 一个类有多个直接基类的继承关系称为多继承
? 多继承声明语法
class 派生类名 : 訪问控制 基类名1 , 訪问控制 基类名2 , … , 訪问控制 基类名n
{
数据成员和成员函数声明
};
? 类 C 能够依据訪问控制同一时候继承类 A 和类B 的成员,并加入
自己的成员
多继承的派生类构造和訪问
? 多个基类的派生类构造函数能够用初始式调用基类构造函数初始化数据成员
? 运行顺序与单继承构造函数情况类似。
多个直接基类构造函数运行顺序取决于定义派生类时指定的各个继承基类的顺序。
? 一个派生类对象拥有多个直接或间接基类的成员。不同名成员訪问不会出现二义性。假设不同的基类有同名成员,派生类对象訪问时应该加以识别。
多继承简单应用
3.4.2虚继承
假设一个派生类从多个基类派生,而这些基类又有一个共同的基类。则在对该基类中声明的名字进行訪问时。可能产生二义性
分析:
总结:
? 假设一个派生类从多个基类派生,而这些基类又有一个共同
的基类,则在对该基类中声明的名字进行訪问时,可能产生
二义性
? 假设在多条继承路径上有一个公共的基类,那么在继承路径的某处
汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象
? 要使这个公共基类在派生类中仅仅产生一个子对象,必须对这个基类
声明为虚继承。使这个基类成为虚基类。
? 虚继承声明使用keyword virtual
实验:注意添加virtualkeyword后,构造函数调用的次数。
3.5继承总结
? 继承是面向对象程序设计实现软件重用的重要方法。程序猿能够在已有基类的基础上定义新的派生类。
? 单继承的派生类仅仅有一个基类。多继承的派生类有多个基类。
? 派生类对基类成员的訪问由继承方式和成员性质决定。
? 创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
? C++提供虚继承机制,防止类继承关系中成员訪问的二义性。
? 多继承提供了软件重用的强大功能,也添加了程序的复杂性。
4、多态
问题引出(赋值兼容性原则遇上函数重写)
面向对象新需求
C++提供的多态解决方式
多态案例
多态project意义
面向对象三大概念、三种境地(封装、继承、多态)
多态成立条件
总结条件、看代码的时候要看出多态
4.1多态
4.1.1问题引出
假设子类定义了与父类中原型同样的函数会发生什么?
函数重写 在子类中定义与父类中原型同样的函数 函数重写仅仅发生在父类与子类之间 |
class Parent { public: void print() { cout<<"Parent:print() do..."<<endl; } }; class Child : public Parent { public: void print() { cout<<"Child:print() do..."<<endl; } }; int main01() { run00(); /* Child child; Parent *p = NULL; p = &child; child.print(); child.Parent::print(); */ system("pause"); return 0; } |
父类中被重写的函数依旧会继承给子类 默认情况下子类中重写的函数将隐藏父类中的函数 通过作用域分辨符::能够訪问到父类中被隐藏的函数 |
/* C/C++是静态编译型语言 在编译时,编译器自己主动依据指针的类型推断指向的是一个什么样的对象 */ /* 1、在编译此函数的时。编译器不可能知道指针 p 到底指向了什么。 2、编译器没有理由报错。 3、于是,编译器觉得最安全的做法是编译到父类的print函数,由于父类和子类肯定都有同样的print函数。 */ //面向对象新需求 //假设我传一个父类对象。运行父类的print函数 //假设我传一个子类对象,运行子类的printf函数 //现象产生的原因 //赋值兼容性原则遇上函数重写 出现的一个现象 //1 没有理由报错 //2 对被调用函数来讲。在编译器编译期间,我就确定了,这个函数的參数是p,是Parent类型的。。 。 //3静态链编 //project开发中怎样推断是不是多态存在? /* 在同一个类里面能实现函数重载 继承的情况下。发生重写 重载不一定; 重写的定义 静态联编 重载是 动态联编 */ #include <iostream> using namespace std; class Parent { public: void print() { cout<<"Parent:print() do..."<<endl; } }; class Child : public Parent { public: void print() { cout<<"Child:print() do..."<<endl; } }; /* 1、在编译此函数的时,编译器不可能知道指针 p 到底指向了什么。 2、编译器没有理由报错。 3、于是,编译器觉得最安全的做法是编译到父类的print函数,由于父类和子类肯定都有同样的print函数。 */ void howToPrint(Parent* p) { p->print(); } void run00() { Child child; Parent* pp = &child; Parent& rp = child; //child.print(); //通过指针 //pp->print(); //通过引用 //rp.print(); howToPrint(&child); } int main01() { run00(); /* Child child; Parent *p = NULL; p = &child; child.print(); child.Parent::print(); */ system("pause"); return 0; } |
4.1.2面向对象新需求
编译器的做法不是我们期望的
依据实际的对象类型来推断重写函数的调用
假设父类指针指向的是父类对象则调用父类中定义的函数
假设父类指针指向的是子类对象则调用子类中定义的重写函数
4.1.3解决方式
? C++中通过virtualkeyword对多态进行支持
? 使用virtual声明的函数被重写后就可以展现多态特性
4.1.4多态实例
案例场景:
英雄战机HeroFighter , AdvHeroFighter 分别和敌机EnemyFighter 战斗.
power() attack()
#include "iostream" using namespace std; class HeroFighter { public: public: virtual int ackPower() { return 10; } }; class AdvHeroFighter : public HeroFighter { public: virtual int ackPower() { return HeroFighter::ackPower()*2; } }; class enemyFighter { public: int destoryPower() { return 15; } }; //假设把这个结构放在动态库里面 //写了一个框架,能够调用 //我的第3代战机代码出现的时间晚于框架出现的时间。。。 。 //框架 有使用后来人 写的代码的能力。。。 //面向对象3大概念 /* 封装 突破了C语言函数的概念。 。 继承 代码复用 。。 。。 我复用原来写好的代码。。。 多态 多态能够使用未来。。。。 。 80年代写了一个框架。。 。 。。。 90人写的代码 多态是我们软件行业追寻的一个目标。 。。 //// */ // void objPK(HeroFighter *hf, enemyFighter *enemyF) { if (hf->ackPower() >enemyF->destoryPower()) { printf("英雄打败敌人。 。 。胜利\n"); } else { printf("英雄。。。 牺牲\n"); } } void main() { HeroFighter hf; enemyFighter ef; objPK(&hf, &ef); AdvHeroFighter advhf; objPK(&advhf, &ef); system("pause"); } |
4.1.5多态project意义
//面向对象3大概念
/*
封装
突破了C语言函数的概念。
。
继承
代码复用 。
。
。。
我复用原来写好的代码。
。。
多态
多态能够使用未来。。。。
。80年代写了一个框架。
。。
。
。。
90人写的代码
多态是我们软件行业追寻的一个目标。。。
//写了一个框架。能够调用后来人。写的代码的能力
////
*/
4.1.6多态成立的条件
//间接赋值成立的3个条件
//1 定义两个变量。。。
//2 建立关联 。。
。。
//3 *p
//多态成立的三个条件
//1 要有继承
//2 要有函数重写。。
。C 虚函数
//3 要有父类指针(父类引用)指向子类对象
//多态是设计模式的基础。多态是框架的基础
4.1.7多态的理论基础
01静态联编和动态联编
1、联编是指一个程序模块、代码之间互相关联的过程。
2、静态联编(static binding)。是程序的匹配、连接在编译阶段实现。也称为早期匹配。
重载函数使用静态联编。
3、动态联编是指程序联编推迟到执行时进行,所以又称为晚期联编(迟绑定)。
switch 语句和 if 语句是动态联编的样例。
4、理论联系实际
1、C++与C同样。是静态编译型语言 2、在编译时,编译器自己主动依据指针的类型推断指向的是一个什么样的对象;所以编译器觉得父类指针指向的是父类对象。 3、因为程序没有执行。所以不可能知道父类指针指向的详细是父类对象还是子类对象 从程序安全的角度,编译器如果父类指针仅仅指向父类对象。因此编译的结果为调用父类的成员函数。这样的特性就是静态联编。 |
4.2多态相关面试题
面试题1:请谈谈你对多态的理解
多态的实现效果 多态:相同的调用语句有多种不同的表现形态; 多态实现的三个条件 有继承、有virtual重写、有父类指针(引用)指向子类对象。 多态的C++实现 virtualkeyword,告诉编译器这个函数要支持多态。不是依据指针类型推断怎样调用;而是要依据指针所指向的实际对象类型来推断怎样调用 多态的理论基础 动态联编PK静态联编。依据实际的对象类型来推断重写函数的调用。 多态的重要意义 设计模式的基础 是框架的基石。 实现多态的理论基础 函数指针做函数參数 C函数指针是C++至高无上的荣耀。C函数指针一般有两种使用方法(正、反)。 多态原理探究 与面试官展开讨论 |
面试题2:谈谈C++编译器是怎样实现多态
c++编译器多态实现原理
面试题3:谈谈你对重写。重载理解
函数重载
必须在同一个类中进行
子类无法重载父类的函数,父类同名函数将被名称覆盖
重载是在编译期间依据參数类型和个数决定函数调用
函数重写
必须发生于父类与子类之间
而且父类与子类中的函数必须有全然同样的原型
使用virtual声明之后可以产生多态(假设不使用virtual,那叫重定义)
多态是在执行期间依据详细对象的类型决定函数调用
#include <cstdlib> #include <iostream> using namespace std; class Parent01 { public: Parent01() { cout<<"Parent01:printf()..do"<<endl; } public: virtual void func() { cout<<"Parent01:void func()"<<endl; } virtual void func(int i) { cout<<"Parent:void func(int i)"<<endl; } virtual void func(int i, int j) { cout<<"Parent:void func(int i, int j)"<<endl; } }; class Child01 : public Parent01 { public: //此处2个參数。和子类func函数是什么关系 void func(int i, int j) { cout<<"Child:void func(int i, int j)"<<" "<<i + j<<endl; } //此处3个參数的。和子类func函数是什么关系 void func(int i, int j, int k) { cout<<"Child:void func(int i, int j, int k)"<<" "<<i + j + k<<endl; } }; void run01(Parent01* p) { p->func(1, 2); } int main() { Parent01 p; p.func(); p.func(1); p.func(1, 2); Child01 c; //c.func(); //问题1 c.Parent01::func(); c.func(1, 2); run01(&p); run01(&c); system("pause"); return 0; } //问题1:child对象继承父类对象的func,请问这句话能执行吗?why //c.func(); //由于名称覆盖。C++编译器不会去父类中寻找0个參数的func函数,仅仅会在子类中找func函数。 //1子类里面的func无法重载父类里面的func //2当父类和子类有同样的函数名、变量名出现,发生名称覆盖(子类的函数名,覆盖了父类的函数名。 ) //3//c.Parent::func(); //问题2 子类的两个func和父类里的三个func函数是什么关系? |
面试题4:是否可类的每一个成员函数都声明为虚函数。为什么。 c++编译器多态实现原理
面试题5:构造函数中调用虚函数能实现多态吗?为什么? c++编译器多态实现原理
面试题6:虚函数表指针(VPTR)被编译器初始化的过程,你是怎样理解的?
c++编译器多态实现原理
面试题7:父类的构造函数中调用虚函数。能发生多态吗? c++编译器多态实现原理
面试题8:为什么要定义虚析构函数?
在什么情况下应当声明虚函数
? 构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根開始,沿着继承路径逐个调用基类的构造函数
? 析构函数能够是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象
其它
父类指针和子类指针的步长
1) 铁律1:指针也仅仅一种数据类型。C++类对象的指针p++/--,仍然可用。
2) 指针运算是依照指针所指的类型进行的。
p++《=》p=p+1 //p = (unsigned int)basep + sizeof(*p) 步长。
3) 结论:父类p++与子类p++步长不同;不要混搭。不要用父类指针++方式操作数组。
4.3多态原理探究
理论知识:
? 当类中声明虚函数时,编译器会在类中生成一个虚函数表
? 虚函数表是一个存储类成员函数指针的数据结构
? 虚函数表是由编译器自己主动生成与维护的
? virtual成员函数会被编译器放入虚函数表中
? 当存在虚函数时,每一个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针。当进行howToPrint(Parent *base)函数是,C++编译器不须要区分子类对象或者父类对象,仅仅须要再base指针中,找vptr指针就可以。
)
? VPTR一般作为类对象的第一个成员
4.3.1 多态的实现原理
C++中多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表 虚函数表是一个存储类成员函数指针的数据结构 虚函数表是由编译器自己主动生成与维护的 virtual成员函数会被编译器放入虚函数表中 存在虚函数时,每一个对象中都有一个指向虚函数表的指针(vptr指针) |
说明1: 通过虚函数表指针VPTR调用重写函数是在程序执行时进行的,因此须要通过寻址操作才干确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上。虚函数的效率要低非常多。 说明2: 出于效率考虑,没有必要将全部成员函数都声明为虚函数 |
说明3 :C++编译器。运行HowToPrint函数。不须要区分是子类对象还是父类对象 |
4.3.2怎样证明vptr指针的存在
#include <iostream> using namespace std; class A { public: void printf() { cout<<"aaa"<<endl; } protected: private: int a; }; class B { public: virtual void printf() { cout<<"aaa"<<endl; } protected: private: int a; }; void main() { //加上virtualkeyword c++编译器会添加一个指向虚函数表的指针 。。。 printf("sizeof(a):%d, sizeof(b):%d \n", sizeof(A), sizeof(B)); cout<<"hello..."<<endl; system("pause"); return ; } |
4.3.3构造函数中能调用虚函数,实现多态吗
1)对象中的VPTR指针什么时候被初始化?
对象在创建的时,由编译器对VPTR指针进行初始化 仅仅有当对象的构造全然结束后VPTR的指向才终于确定 父类对象的VPTR指向父类虚函数表 子类对象的VPTR指向子类虚函数表 |
2)分析过程 绘图分析 |
5、纯虚函数和抽象类
5.1基本概念
5.2抽象类案例
5.3抽象类在多继承中的应用
C++中没有Java中的接口概念,抽象类能够模拟Java中的接口类。(接口和协议)
5.3.1有关多继承的说明
project上的多继承
被实际开发经验抛弃的多继承
project开发中真正意义上的多继承是差点儿不被使用的
多重继承带来的代码复杂性远多于其带来的便利
多重继承对代码维护性上的影响是灾难性的
在设计方法上,不论什么多继承都能够用单继承取代
多继承中的二义性和多继承不能解决的问题
5.3.2多继承的应用场景
C++中是否有Java中的接口概念? |
绝大多数面向对象语言都不支持多继承 绝大多数面向对象语言都支持接口的概念 C++中没有接口的概念 C++中能够使用纯虚函数实现接口 接口类中仅仅有函数原型定义,没有不论什么数据的定义。 class Interface { public: virtual void func1() = 0; virtual void func2(int i) = 0; virtual void func3(int i) = 0; }; |
实际project经验证明 多重继承接口不会带来二义性和复杂性等问题 多重继承能够通过精心设计用单继承和接口来取代 接口类仅仅是一个功能说明。而不是功能实现。 子类须要依据功能说明定义功能实现。 |
#include "iostream" using namespace std; /* C++中没有接口的概念 C++中能够使用纯虚函数实现接口 接口类中仅仅有函数原型定义,没有不论什么数据的定义。 */ class Interface1 { public: virtual void print() = 0; virtual int add(int a, int b) = 0; }; class Interface2 { public: virtual void print() = 0; virtual int add(int a, int b) = 0; virtual int minus(int a, int b) = 0; }; class parent { public: int a; }; class Child : public parent, public Interface1, public Interface2 { public: void print() { cout<<"Child::print"<<endl; } int add(int a, int b) { return a + b; } int minus(int a, int b) { return a - b; } }; int main() { Child c; c.print(); cout<<c.add(3, 5)<<endl; cout<<c.minus(4, 6)<<endl; Interface1* i1 = &c; Interface2* i2 = &c; cout<<i1->add(7, 8)<<endl; cout<<i2->add(7, 8)<<endl; system("pause"); } |
5.4抽象类知识点强化
/*
编写一个C++程序, 计算程序猿( programmer )工资
1要求能计算出0基础程序猿( junior_programmer ) 中级程序猿 ( mid_programmer )高级程序猿( adv_programmer)的工资
2要求利用抽象类统一界面,方便程序的扩展, 比方:新增, 计算架构师 (architect ) 的工资
*/
5.5面向抽象类编程思想强化
理论知识
? 虚函数和多态性使成员函数依据调用对象的类型产生不同的动作
? 多态性特别适合于实现分层结构的软件系统,便于对问题抽象时 定义共性,实现时定义差别
? 面向抽象类编程(面向接口编程)是项目开发中重要技能之中的一个。
5.4.1案例:socket库c++模型设计和实现
企业信息系统框架集成第三方产品
案例背景:一般的企业信息系统都有成熟的框架。软件框架一般不发生变化,能自由的集成第三方厂商的产品。
案例需求:请你在企业信息系统框架中集成第三方厂商的Socket通信产品和第三方厂商加密产品。
第三方厂商的Socket通信产品:完毕两点之间的通信;
第三方厂商加密产品:完毕数据发送时加密。数据解密时解密。
案例要求: 1)能支持多个厂商的Socket通信产品入围
2)能支持多个第三方厂商加密产品的入围
3)企业信息系统框架不轻易发生框架
需求实现
思考1:企业信息系统框架、第三方产品怎样分层
思考2:企业信息系统框架。怎样自由集成第三方产品
(软件设计:模块要求松、接口要求紧)
思考3:软件分成以后。开发企业信息系统框架的程序猿,应该做什么?
第三方产品入围应该做什么?
编码实现
分析有多少个类 CSocketProtocol CSckFactoryImp1 CSckFactoryImp2
CEncDesProtocol HwEncdes ciscoEncdes
1、 定义CSocketProtocol 抽象类
2、 编写框架函数
3、 编写框架測试函数
4、 厂商1(CSckFactoryImp1)实现CSocketProtocol、厂商2(CSckFactoryImp1)实现CSocketProtocol
5、 抽象加密接口(CEncDesProtocol)、加密厂商1(CHwImp)、加密厂商2(CCiscoImp)),集成实现业务模型
6、 框架(c语言函数方式。框架函数;c++类方式。框架类)
几个重要的面向对象思想
继承-组合(强弱)
注入
控制反转 IOC
MVC
面向对象思想扩展aop思想
aop思想是对继承编程思想的有力的补充
5.4.2案例:计算员工工资
5.4.3案例:计算几何体的表面积和体积
5.6 C面向接口编程和C多态
友情提示:今天课程内容,更加贴近实战,而且语法和软件思想都较难,请学员紧跟思路。课后加强复习!
结论: 仅仅要你动手,又非常easy!
5.6.1函数类型语法基础
函数三要素: 名称、參数、返回值 C语言中的函数有自己特定的类型 |
C语言中通过typedef为函数类型重命名 typedef type name(parameter list) typedef int f(int, int); typedef void p(int); |
函数指针 |
函数指针用于指向一个函数 函数名是函数体的入口地址 1)可通过函数类型定义函数指针: FuncType* pointer; 2)也能够直接定义:type (*pointer)(parameter list); pointer为函数指针变量名 type为指向函数的返回值类型 parameter list为指向函数的參数类型列表 |
函数指针语法梳理 //函数类型 //函数指针类型 //函数指针变量 数组指针语法梳理 //数组类型语法 //数组指针类型 //数组指针变量 |
typedef int(FUNC)(int); int test(int i) { return i * i; } void f() { printf("Call f()...\n"); } int main() { FUNC* pt = test; void(*pf)() = &f; pf(); (*pf)(); printf("Function pointer call: %d\n", pt(3)); } |
5.6.2函数指针做函数參数
1、 指针做函数參数pk函数指针做函数參数 回顾指针做函数參数 一级指针做函数參数、二级。 。 。。、三级 |
2、 函数指针做函数參数 当函数指针 做为函数的參数,传递给一个被调用函数。 被调用函数就能够通过这个指针调用外部的函数,这就形成了回调 |
3、练习 int add(int a, int b) int libfun( int (*pDis)(int a, int b) ); int main(void) { int (*pfun)(int a, int b); pfun = add; libfun(pfun); } int add(int a, int b) { return a + b; } int libfun( int (*pDis)(int a, int b) ) { int a, b; a = 1; b = 2; add(1,3) //直接调用add函数 printf("%d", pDis(a, b)); //通过函数指针做函数參数,间接调用add函数 //思考 这样写 pDis(a, b)有什么优点? } //剖析思路 //1函数的调用 和 函数的实现 有效的分离 //2 C++的多态,可扩展 如今这几个函数是在同一个文件其中 假如 int libfun(int (*pDis)(int a, int b)) 是一个库中的函数,就仅仅有使用回调了。通过函数指针參数将外部函数地址传入 来实现调用 函数 add 的代码作了修改,也不必修改库的代码,就能够正常实现调用 便于程序的维护和升级 |
回调函数思想:
结论:回调函数的本质:提前做了一个协议的约定(把函数的參数、函数返回值提前约定)
请思考:C编译器通过那个详细的语法。实现解耦合的?
C++编译器通过多态的机制(提前布局vptr指针和虚函数表,找虚函数入口地址来实现)
5.6.3函数指针正向调用
1、 函数指针做函数參数,调用方式 被调用函数和主调函数在同一文件里(用来教学,没有不论什么意义) |
2、函数指针做函数參数 被调用函数和主调函数不在同一个文件里、模块中。 难点:理解被调用函数是什么机制被调用起来的。 框架 框架提前设置了被调用函数的入口(框架提供了第三方模块入口地址的集成功能) 框架具备调用第三方模块入口函数 |
3、 练习 typedef int (*EncDataFunc)(unsigned char *inData,int inDataLen,unsigned char *outData,int *outDataLen,void *Ref, int RefLen); int MyEncDataFunc(unsigned char *inData,int inDataLen,unsigned char *outData,int *outDataLen,void *Ref, int RefLen) { int rv = 0; char *p = "222222222222"; strcpy(outData, p); *outDataLen = strlen(p); return rv; } int Send_Data(EncDataFunc encDataFunc, unsigned char *inData, int inDataLen, unsigned char *outData, int *outDatalen) { int rv = 0; if (encDataFunc != NULL) { rv = encDataFunc(inData, inDataLen, outData, outDatalen, NULL, 0); if (rv != 0) { printf("func encDataFunc() err.\n"); return rv; } } return rv; } int main() { int rv = 0; EncDataFunc encDataFunc = NULL; encDataFunc = MyEncDataFunc; // 第一个调用 { unsigned char inData[2048]; int inDataLen; unsigned char outData[2048]; int outDatalen; strcpy(inData, "1111"); inDataLen = strlen(inData); rv = encDataFunc(inData,inDataLen, outData, &outDatalen, NULL, 0); if (rv != 0) { printf("edf err .....\n"); } else { printf("edf ok \n"); printf("%s \n", outData); } } { unsigned char inData[2048]; int inDataLen; unsigned char outData[2048]; int outDatalen; strcpy(inData, "3333"); inDataLen = strlen(inData); rv = Send_Data(MyEncDataFunc, inData, inDataLen, outData, &outDatalen); if (rv != 0) { printf("func Send_Data err:%d", rv); return rv; } printf("%s \n", outData); } getchar(); } |
5.6.4函数指针反向调用
回调函数效果展示。
5.6.5.C动态库升级成框架案例
C语言版本号Socket动态库升级成框架集成第三方产品
简称:C动态库升级成框架案例
名字解释
动态库:抽象类一个套接口,单独封装成模块,供别人调用;无法扩展。
框架:能自由的扩展
案例背景:一般的企业信息系统都有成熟的框架,能够有C语言写,也能够由C++语言。软件框架一般不发生变化。能自由的集成第三方厂商的产品。
案例需求:在socket通信库中,完毕数据加密功能,有n个厂商的加密产品供你选择。怎样实现动态库和第三个厂商产品的解耦合。
提醒:C++通过抽象类,也就是面向抽象类编程实现的(相当于C++编译器通过多态机制,已经非常好用了。提前布局vptr指针、虚函数表;调用是迟绑定完毕。),
C语言中怎样实现哪?
案例要求: 1)能支持多个第三方厂商加密产品的入围
2)企业信息系统框架不轻易发生框架
需求实现思路分析
思考1:企业信息系统框架、第三方产品怎样分层
思考2:企业信息系统框架,怎样自由集成第三方产品
(软件设计:模块要求松、接口要求紧)
思考3:软件分层确定后,动态库应该做什么?产品入围厂商应该做什么?
以后。开发企业信息系统框架的程序猿,应该做什么?
第三方产品入围应该做什么?
编码实现
1、 动态库中定义协议。并完毕任务的调用
typedef int (*EncData)(unsigned char *inData,int inDataLen,unsigned char*outData,int *outDataLen,void *Ref, int RefLen);
typedef int (*DecData)(unsigned char *inData,int inDataLen,unsigned char*outData,int *outDataLen,void *Ref, int RefLen);
2、 加密厂商完毕协议函数的编写
3、 对接调试。
4、 动态库中能够缓存第三方函数的入口地址。也能够不缓存。两种实现方式。
案例总结
回调函数:利用函数指针做函数參数,实现的一种调用机制。详细任务的实现者,能够不知道什么时候被调用。
回调机制原理:
当详细事件发生时,调用者通过函数指针调用详细函数
回调机制的将调用者和被调函数分开。两者互不依赖
任务的实现 和 任务的调用 能够耦合 (提前进行接口的封装和设计)
5.6.6附录:诸葛亮的锦囊妙计
刘备利用周瑜、曹仁厮杀之际。乘虚袭取了南郡、荆州、襄阳,以后又征服了长沙等四郡。
周瑜想想十分气恨,正无处报复以夺还荆州。不久。刘备忽然丧偶。周瑜计上心来,对孙权说:“您的妹妹,漂亮、刚强,我们以联姻抗曹名义向刘备招亲,把他骗来南徐幽禁,逼他们拿荆州来换。”孙权大喜,郎派人到荆州说亲。
刘备觉得这是骗局,想要拒绝。诸葛亮笑道:“送个好妻子上门何不答应?您仅仅管去东吴,我叫赵云陪您去,自有安排,包您得了夫人又不失荆州。”
接着。诸葛亮暗暗关照赵云道:“我这里有三个锦囊。内藏三条妙计。到南徐时打开第一个,到年底时打开第二个。危险无路时打开第三个。
”
第一个锦囊
一到东吴就拜会乔国老
第二个锦囊
刘备被孙权设计留下就对他谎称曹操大军压境
第三个锦囊
被东吴军队追赶就求孙夫人解围