C++学习笔记
- 1.C++综述
- 2.C++对C语言的加强
- 3.C++对C语言的拓展
- 4.类和对象
- 5.继承和派生
- 6.多态
1.C++综述
1.1 作者
1982年,美国AT&T公司贝尔实验室的 Bjarne Stroustrup 博主在c语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,他被命名为C++。而 Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)博士被尊称 为 C++语言之父。
1.2 历史背景
1.2.1 应“运”而生?运为何?
C语言作为结构化和模块化语言,在处理较小规模的程序时,比较得心应手。但是当处理比较复杂且规模比较大的问题时,需要高度的抽象和建模时,C语言显得力不重新。
为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming)思想,这需要设计出支持面向对象的程序设计语言。Smalltalk 就是当时问世的一种面向对象的语言。而在实践中,人们发现 c 是语此深入人心,使用如此之广 泛,以至于最好的办法,不是发明一种新的语言去取代它,而是在原有的基础上发展它。 在这种情况下 c++应运而生,最初这门语言并不叫 c++而是 c with class (带类的 c)。
1.2.2 C++发展大记事
序号 | 事件 |
---|---|
1 | 1983 年 8 月, 第一个 C++实现投入使用 |
2 | 1983 年 12 月,Rick Mascitti 建议命名为 CPlusPlus,即 C++。 |
3 | 1985 年 2 月, 第一个 C++ Release E 发布。 |
4 | 10 月,CFront 的第一个商业发布,CFront Release 1.0。 |
5 | 10 月,Bjarne 博士完成了经典巨著 The C++ Programming Language 第一版 1986 年 11 月,C++第一个商业移植 CFront 1.1,Glockenspiel。 |
6 | 1987 年 2 月, CFront Release 1.2 发布。11 月,第一个 USENIX C++会议在新墨西哥州举行。 |
7 | 1988 年 10 月,第一次 USENIX C++实现者工作会议在科罗拉多州举行 |
8 | 1989 年 12 月,ANSI X3J16 在华盛顿组织会议。 |
9 | 1990 年 3 月, 第一次 ANSI X3J16 技术会议在新泽西州召开. |
10 | 1990 年 5 月, C++的又一个传世经典 ARM 诞生。 1990 年 7 月, 模板被加入。 1990 年 11 月,异常被加入。 |
11 | 1991 年 6 月, The C++ Programming Language 第二版完成。 |
12 | 1991 年 6 月, 第一次 ISO WG21 会议在瑞典召开。 |
13 | 1991 年 10 月,CFront Release 3.0 发布。 |
14 | 1993 年 3 月, 运行时类型识别在俄勒冈州被加入。 |
15 | 1993 年 7 月, 名字空间在德国慕尼黑被加入。 |
16 | 1994 年 8 月, ANSI/ISO 委员会草案登记。 |
17 | 1997 年 7 月, The C++ Programming Language 第三版完成。 |
18 | 1997 年 10 月,ISO 标准通过表决被接受 |
19 | 1998 年 11 月,ISO 标准被批准。 |
1.3 应用领域
如果项目中,既要求效率又要建模和高度抽像,那就选择 c++吧。
1.3.1 系统层软件开发
C++的语言本身的高效。
1.3.2 服务器程序开发
面向对像,具有较强的抽像和建模能力。
1.3.3 游戏,网络,分布式,云计算
效率与建模 。
1.3.4 科学计算
比如大名鼎鼎的 ACE 等科学类库。
1.4 内容
C++语言的名字,如果看作 c 的基本语法,是由操作数 c 和运算符后++构成。C++ 是本身这门语言先是 c,是完全兼容 c.然后在此基础上++。这个++包含三大部分,c++对 c 的基础语法的扩展,面向对像(继承,封装,多态),STL 等。
a.对C语言基础语法扩展;
b.增加了面向对象的思想(类和对象 ,继承和派生 , 多态);
c.增加了模版的概念。
1.5 书籍推荐
2.C++对C语言的加强
2.1 namespace命名空间
2.1.1 C++命名空间基本常识
namespace 基本概念如下:
是指标识符的各种可见范围。
一、<iostream>和<iostream.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) 使用using关键字
using std::cout;
using std::endl;
using std::cin;
3) 最方便的就是使用using namespace std; 这样命名空间std内定义的所有标识符都有效曝光,就好像声明了全局变量一样。
2.1.2 C++命名空间定义以及使用方法
在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等。
为了避免,大规模程序设计中,以及程序员使用各种各样的C++库时,这些标识符的命名发生冲突。标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),更好的控制标识符的作用域。
标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。
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;
默认情况下可以直接使用默认命名空间中的所有标识符
2.1.3 C++命名空间编程实践
#include <stdio.h>
namespace NameSpaceA
{
int a = 0;
};
namespace NameSpaceB
{
int a = 1;
namespace NameSpaceC
{
struct Teacher
{
char name[10];
int age;
};
}
};
int main(int argc,char **argv)
{
using namespace NameSpaceA;
using NameSpaceB::NameSpaceC::Teacher;
printf("a = %d\r\n",a);
printf("a = %d\r\n",NameSpace::a);
Teacher t1 = {"aaa",3};
printf("t1.name = %s\r\n",t1.name);
printf("t1.age = %d\r\n",t1.age);
return 0;
}
2.1.4 总结
a) 当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。若不引入using namespace std,需要这样做。std::cout。
b) c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用.h后缀。
c) c++命名空间定义,namespace name{...};
d) using namespace NameSpace;
e) namespace 定义可嵌套。
2.2 “实用性”增强
1.在C语言中变量都必须在作用域开始的位置定义;
2.C++中更强调语言的“实用性”,所有的变量都可以在需要使用时在定义;
#include <iostream>
using namespace std;
int main(void)
{
int i = 0;
cout <<"i = " << i << endl;
int k = 4;
cout << "k = " << k << endl;
return 0;
}
2.3 变量检测增强
1.在C语言中,重复定义多个同名的全局变量是合法的;
2.在C++中,不允许定义多个同名的全局变量;
3.C语言中,多个同名的全局变量最终会被链接到全局数据区的同一个地址空间上。
int g_Var;
int g_Var = 1;
#include <iostream>
int g_Var;
int g_Var = 1;
int main(int argc,char **argv)
{
printf("g_Var = %d\r\n",g_Var);
}
2.4 struct类型增强
C语言中定义了一组变量的集合,C编译器并不认为这是一种新的变量类型;
C++中的struct是一个新类型的定义声明。
#include <iostream>
struct Student
{
char name[256];
int age;
};
int main(int argc,char **argv)
{
Student s1 = {"YJ Is Dog",1};
Student s2 = {"WL Is Pig",1};
return 0;
}
2.5 C++中所有变量和函数都必须有类型
C++中所有变量和函数都必须有类型
C语言中的默认类型在C++中是不合法的
2.6 新增bool类型关键字
2.7 三目运算符功能增强
2.8 const增强
2.8.1 const基础知识
#include <iostream>
int main(int argc,char **argv)
{
//const 定义常量 ----> const 意味着只读
const int a;
int const b;
//第一,第二含义一样,表示一个整形常数
const int *c;
//第三个c 是一个指向常整形数的指针(所指向的内存数据不能被修改,但是本身可以被修改)。
int *const d;
//第四个 d 常指针(指针变量不能被修改,但是所指向的内存空间可以被修改)
const int *const e;
//第五个 e 指向常整形的常指针(指针和它所指向的内存空间,均不能被修改)
return 0;
}
理解技巧:从const关机字往后看,const后所表示的数据类型,是常量,不能被修改。
合理利用const的好处:
a) 指针做函数参数,可以有效提高代码可读性,减少bug;
b) 清楚的分清楚参数的输入和输出特性。
int setTeacher_err(const Teacher *p)
const 修饰形参时,在利用形参不能修改指针所指向的内存空间
2.8.2 C语言中的"冒牌货"
#include <stdio.h>
int main(int argc,char **argv)
{
const int a = 10;
int *p = (int *)&a;
printf("a = %d\r\n",a);
a = 11;
printf("a = %d\r\n",a);
return 0;
}
2.8.3 const 和 #define的相同
#include <iostream>
int main(int argc,char **argv)
{
const int a = 1;
const int b = 2;
int array[a+b] = {0x00};
int i = 0;
for(i = 0;i < (a+b);i++)
{
printf("array[%d] = %d\r\n",i,array[i]);
}
return 0;
}
C++ 中的const修饰,是一个真正的常量,而不是C中的变量(只读)。在const修饰的常量编译期间,就已经确定下来了。
2.8.4 const 和 #define的区别
#include <iostream>
void fun1(void)
{
#define a 10
const int b = 20;
}
void fun2(void)
{
printf("a = %d\r\n",a);
}
int main(int argc,char **argv)
{
fun1();
fun2();
return 0;
}
C++中的const类似于宏定义
const int c = 5; ≈ define c 5
C++中的const常量与宏定义不同
const常量是由编译器处理的,提供类型检查和作用域检查;
宏定义由预处理器处理,单纯的文本替换。
2.8.4 关于const的总结
C语言中的const变量
C语言中的const变量是只读变量,有自己的存储空间。
C++中的const变量
可能分配存储空间,也可能不分配存储空间。
当const常量为全局变量,并且需要在其他文件中使用时,会分配存储空间。
当使用&操作符,取const常量地址时,会分配存储空间。
当const int &a = 10;const修饰引用时,也会分配存储空间。
2.9 真正的枚举
c 语言中枚举本质就是整型,枚举变量可以用任意整型赋值。而 c++中枚举变量, 只能用被枚举出来的元素初始化。
#include <iostream>
using namespace std;
enum season {SPR,SUM,AUT,WIN};
int main(void)
{
enum season s = SPR;
s = SUM;
cout << "s = " << s << endl;
return 0;
}
3.C++对C语言的拓展
3.1 引用
3.1.1 变量名
a.变量名实质上是一段连续存储空间的别名,是一个标号(门牌号);
b.通过变量来申请并命名内存空间;
c.通过变量的名字可以使用存储空间。
问题:对一段连续的内存空间只能取一个别名吗?
3.1.2 引用的概念
变量名,本身是一段内存的引用,即别名(alias). 引用可以看作一个已定义变量的别名。
引用的语法:Type& name = var;
用法如下:
#include <iostream>
using namespace std;
int main(int argc,char **argv)
{
int a = 10;
int &b = a; //b就是变量a 的别名
return 0;
}
3.1.3 规则
1 引用没有定义,是一种关系型声明。声明它和原有某一变量(实体)的关系。故 而类型与原类型保持一致,且不分配内存。与被引用的变量有相同的地址。
2 声明的时候必须初始化,一经声明,不可变更。
3 可对引用,再次引用。多次引用的结果,是某一变量具有多个别名。
4 &符号前有数据类型时,是引用。其它皆为取地址。
#include <iostream>
int main(void)
{
int a,b;
int &r = a;
int &r = b; //错误,不可以改变原有的引用关系
float &rr = b; //错误,引用类型不匹配
int &ra = r; //可以对引用多次引用
return 0;
}
3.1.4 引用作为函数参数
普通引用在声明时,必须用其他的变量进行初始化,引用作为函数参数声明时不进行初始化。
#include <iostream>
using namespace std;
struct Teacher
{
char name[64];
int age;
};
void printfT(Teacher *pT)
{
cout << pP -> age<< endl;
}
//pT是t1的别名,相当于修改了t1
void printfT2(Teacher &pT)
{
pT.age = 33;
cout << pT.age << endl;
}
//pT和t1是两个相同的变量
void printfT3(Teacher pT)
{
cout << pT.age << endl;
pT.age = 45;
}
int main(void)
{
Teacher t1;
t1.age = 35;
printfT(&t1);
printfT2(t1); //pT是t1的别名,相当于修改了t1
printf("t1.age:%d\r\n",t1.age);
printfT3(t1); //pT是形参,t1 copy一份数据 给pT
printf("t1.age:%d\r\n",t1.age);
return 0;
}
3.1.5 引用的意义
1)引用作为其它变量的别名而存在,因此在一些场合可以代替指针
2) 引用相对于指针来说具有更强的可读性和实用性
3) c++中引入引用后,可以用引用解决的问题。避免用指针来解决。
3.1.6 引用的本质
#include <iostream>
int main(void)
{
int a = 10;
int &b = a; //注意但对定义的引用,必须进行初始化
b = 11;
printf("a:%d\n",a);
printf("b:%d\n",b);
printf("&a:%p\n",&a);
printf("&b:%p\n",&b);
return 0;
}
1)引用在C++的内部是一个常指针
Type &name <===> Type *const name
2) C++编译器在编译编译过程中,使用常指针作为引用的内部实现,因此,引用所占的空间大小与指针相同。
3)从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了使用性而做出的细节隐藏。
3.1.7 引用作为函数的返回值(引用当左值)
1) 说明:若返回栈变量,不能成为其它引用的初始值(不能作为左值使用)
#include <iostream>
using namespace std;
int getA1()
{
int a;
a = 10;
return a;
}
int &getA2()
{
int a;
a = 10;
return a;
}
int main(void)
{
int a1 = 0;
int a2 = 0;
a1 = getA1();
//将一个引用赋值给另一个变量,会有拷贝动作
//理解:编译器类似做了如下隐藏操作,a2 = *(getA2())
a2 = getA2();
//将一个引用赋给另一个引用作为初始值,由于是栈的引用,内存非法
int &a3 = getA2();
cout<<"a1="<<a1<<endl;
cout<<"a2="<<a2<<endl;
cout<<"a3="<<a3<<endl;
return 0;
}
2) 说明:若返回静态变量或全局变量 ,可以成为其他引用的初始值(可作为右值使用,也可作为左值使用)
#include <iostream>
using namespace std;
int getA1(void)
{
static int a;
a = 10;
return a;
}
int &getA2(void)
{
static int a;
a = 10;
return a;
}
int main(void)
{
int a1 = 0;
int a2 = 0;
//值拷贝
a1 = getA1();
//将一个引用赋值给一个变量,会有拷贝动作
//理解:编译器类似做了如下隐藏操作,a2 = *(getA2())
a2 = getA2();
//将一个引用赋值给另外一个引用作为初始值,由于是静态区域,内存合法
int &a3 = getA2();
cout <<"a1 = "<< a1 <<endl;
cout <<"a2 = "<< a2 <<endl;
cout <<"a3 = "<< a3 <<endl;
return 0;
}
3) 说明:引用作为函数返回值,如果返回值为引用可以当左值,如果返回值为普通变量不可以当左值。
#include <iostream>
using namespace std;
//函数当左值
//返回变量的值
int fun1()
{
static int a1 = 10;
return a1;
}
//返回变量本身
int &fun2()
{
static int a2 = 10;
return a2;
}
int main(void)
{
//函数当右值
int c1 = func1();
cout << "c1 = " << c1 << endl;
int c2 = func2(); //函数返回值是一个引用,并且当右值
cout << "c2 = " << c2 << endl;
func2() = 100; //函数返回值是一个引用,并且当左值
return 0;
}
3.1.8 指针引用
#include <iostream>
using namespace std;
struct Teacher
{
char name[64];
int age;
};
//在被调用函数 获取资源
int getTeacher(Teacher **p)
{
Teacher *tmp = NULL;
if(p == NULL)
{
return -1;
}
tmp = (Teacher *)malloc(sizeof(Teacher));
if(tmp == NULL)
{
return -2;
}
tmp -> age = 33;
*p = tmp;
return 0;
}
int getTeacher2(Teacher * &myp)
{
myp = (Teacher *)malloc(sizeof(Teacher));
if(myp == NULL)
{
return -1;
}
myp->age = 36;
return 0;
}
void FreeTeacher(Teacher *pT1)
{
if(pT1 == NULL)
{
return ;
}
free(pT1);
}
int main(void)
{
Teacher *pT1 = NULL;
//1. c语言中的二级指针
getTeacher(&pT1);
FreeTeacher(pT1);
cout <<"age :" <<pT1->age<<endl;
//2. C++中的引用 (指针的引用)
//引用的本质,间接赋值后2个条件,让C++编译器帮助我们程序员
return 0;
}
3.1.9 const引用
const引用有较多的使用。它可以防止对象的值随意被修改。因而具有一些特性
a) const 对象的引用必须是 const 的,将普通引用绑定到 const 对象是不合法的。如:
const int a = 1;
int &b = a;
b) const 引用可使用相关类型的对象(常量,非同类型的变量或表达式)初始化。
#include <iostream>
using namespace std;
int main(int argc,char **argv)
{
//普通引用
int a = 10;
int &b = a;
cout << "b = " << b << endl;
//常引用
int x = 20;
const int &y = x; //常引用是限制了变量为只读,不能通过y去修改x了
return 0;
}
3.1.10 const引用原理
const 引用的目的是,禁止通过修改引用值来改变被引用的对象。const 引用的 初始化特性较为微妙,可通过如下代码说明:
double val = 3.14;
const int &ref = val;
double &ref2 = val;
val = 4.14;
cout << ref << "" << ref2 << endl;
上述输出结果为 ref = 3.14 和 ref2=4.14。因为 ref 是 const 的,在初始化的过程中已经给定值,不允许修改。而被引用的对象是 val,是非 const 的,所以 val 的修改并未影响ref的值,而 ref2 的值发生了相应的改变。
那么,为什么非 const 的引用不能使用相关类型初始化呢?实际上,const 引用 使用相关类型对象初始化时发生了如下过程:
int temp = val;
const int &ref = temp;
如果 ref 不是 const 的,那么改变 ref 值,修改的是 temp,而不是 val。期望对 ref 的赋值会修改 val 的程序员会发现 val 实际并未修改。
#include <iostream>
using namespace std;
struct Teacher
{
char name[64];
int age;
};
void printTeacher(const Teacher &myt)
{
printf("myt.age = %d\r\n",myt.age);
}
int main(void)
{
Teacher t1;
t1.age = 36;
printTeacher(t1);
return 0;
}
3.1.11 关于引用的总结
结论:
1) const int &e 相当于 const int *const e
2) 普通引用 相当于 int *const e
3) 当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
4) 使用字面量对const引用初始化后,将生成一个只读变量。
3.2 inline内联函数
c 语言中有宏函数的概念。宏函数的特点是内嵌到调用代码中去,避免了函数调用的开销。但是由于宏函数的处理发生在预处理阶段,缺失了语法检测和有可能带来的语意差错。
3.2.1 C++内联函数基本概念
C++提供了 inline 关键字,实现了真正的内嵌。
#include <iostream>
using namespace std;
inline void func(int a)
{
a = 20;
cout << a << endl;
}
int main(void)
{
func(10);
return 0;
}
特点:
1)内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。
2) C++编译器直接将函数体插入在函数调用的地方 。
3) 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。
4) 内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)。
5) 内联函数由编译器处理,直接将编译后的函数体插入调用的地方, 宏代码片段由预处理器处理, 进行简单的文本替换,没有任何编译过程。
6) C++中内联编译的限制:
不能存在任何形式的循环语句
不能存在过多的条件判断语句
函数体不能过于庞大
不能对函数进行取址操作
函数内联声明必须在调用语句之前
7) 编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
3.2.2 内联函数VS宏函数
#include <iostream>
#incldue <string.h>
using namespace std;
#if 0
优点:内嵌代码,辟免压栈与出栈的开销
缺点:代码替换,易使⽣成代码体积变⼤,易产⽣逻辑错误。
#endif
#define SQR(x) ((x)*(x))
#if 0
优点:⾼度抽象,避免重复开发
缺点:压栈与出栈,带来开销
#endif
inline int sqr(int x)
{
return x*x;
}
int main()
{
int i=0;
while(i<5)
{
//printf("%d\n",SQR(i++));
}
return 0;
}
3.2.3 内联函数总结
优点:避免调用时的额外开销(入栈与出栈操作)
代价:由于内联函数的函数体在代码段中会出现多个“副本”,因此会增加代码段的空间。
本质:以牺牲代码段空间为代价,提高程序的运行时间的效率。
适用场景:函数体很“小”,且被“频繁”调用。
3.3 默认参数和占位参数
通常情况下,函数在调用时,形参从实参那里取得值。对于多次调用用一函数同一 实参时,C++给出了更简单的处理办法。给形参以默认值,这样就不用从实参那里取值了。
3.3.1 单个默认参数
void myPrint(int x = 3)
{
cout << "x" << x << endl;
}
3.3.2 多个默认参数
float volume(float length,float weight=4,float high=5)
{
return length*weight*high;
}
int main(void)
{
float v = volume(10);
float v1 = volume(10,20);
float v2 = volume(10,20,30);
cout<<v<<endl;
cout<<v1<<endl;
cout<<v2<<endl;
return 0;
}
3.3.3 默认参数规则
只有参数列表后面部分的参数才可以提供默认参数值
一旦在一个函数调用中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值
3.3.4 占位参数
#include <iostream>
/*
函数占位参数
占位参数只有参数类型声明,⽽没有参数名声明
⼀般情况下,在函数体内部⽆法使⽤占位参数
*/
int func(int a,int b,int)
{
return a+b;
}
int main(void)
{
func(1,2); //error, 必须把最后⼀个占位参数补上。
printf("func(1, 2, 3) = %d\n", func(1, 2, 3));
}
#include <iostream>
/*
可以将占位参数与默认参数结合起来使⽤意义
为以后程序的扩展留下线索兼容C语⾔程序中可能出现的不规范写法
*/
//C++可以声明占位符参数,占位符参数⼀般⽤于程序扩展和对C代码的兼容
int func2(int a, int b, int = 0)
{
return a + b;
}
int main(void)
{
//如果默认参数和占位参数在⼀起,都能调⽤起来
func2(1,2);
func2(1,2,3);
return 0;
}
/* 结论:如果默认参数和占位参数在⼀起,都能调⽤起来 */
3.4 函数重载
函数重载(Function Overload):用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同。
3.4.1 重载规则
1) 函数名相同;
2) 参数个数不同,参数的类型不同,参数顺序不同,均可构成重载;
3) 返回值类型不同则不可以构成重载。
void func(int a); //ok
void func(char a); //ok
void func(char a,int b); //ok
void func(int a, char b); //ok
3.4.2 调用准则
1,严格匹配,找到则调用。
2,通过隐式转换寻求一个匹配,找到则调用。
#include <iostream>
using namespace std;
void print(double a)
{
cout<<a<<endl;
}
void print(int a)
{
cout<<a<<endl;
}
int main(void)
{
print(1); // print(int)
print(1.1); // print(double)
print('a'); // print(int)
print(1.11f); // print(double)
return 0;
}
编译器调用重载函数的准则:
1.将所有同名函数作为候选者
2.尝试寻找可行的候选函数
3.精确匹配实参
4.通过默认参数能够匹配实参
5.通过默认类型转换匹配实参
6.匹配失败
7.最终寻找到的可行候选函数不唯一,则出现二义性,编译失败。
8.无法匹配所有候选者,函数未定义,编译失败。
3.4.3 重载底层实现(name mangling)
C++利用 name mangling(倾轧)技术,来改名函数名,区分参数不同的同名函数。
实现原理:用vcifld表示 void char int float long double 及其引用。
void func(char a); //func_c(char a)
void func(char a,int b,double c); //func_cid(char a, int b, double c)
3.4.4 函数重载与函数默认参数
一个函数,不能既作重载,又作默认参数的函数。当你少写一个参数时,系统 无法确认是重载还是默认参数。
#include <iostream>
using namespace std;
int func(int a, int b,int c = 0)
{
return a*b*c;
}
int func(int a,int b)
{
return a+b;
}
int func(int a)
{
return a;
}
int main()
{
int c = 0;
c = func(1, 2); //error. 存在⼆义性,调⽤失败,编译不能通过
return 0;
}
3.4.5 函数重载和函数指针结合
/*
函数重载与函数指针
当使⽤重载函数名对函数指针进⾏赋值时
根据重载规则挑选与函数指针参数列表⼀致的候选者
严格匹配候选者的函数类型与函数指针的函数类型
*/
#include <iostream>
using namespace std;
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)
typedef int(*PFUNC2)(int a,int b); // int(*)(int a, int b)
int main()
{
int c = 0;
PFUNC p = func;
c = p(1);
printf("c = %d\n", c);
PFUNC2 p2 = func;
c = p2(1,2);
printf("c = %d\n", c);
return 0;
}
函数指针基本语法
//⽅法⼀:
//声明⼀个函数类型
typedef void (myTypeFunc)(int a,int b);
//定义⼀个函数指针
myTypeFunc *myfuncp = NULL;
//定义⼀个函数指针 这个指针指向函数的⼊⼝地址
//⽅法⼆:
//声明⼀个函数指针类型
typedef void (*myPTypeFunc)(int a,int b);//声明了⼀个指针的数据类型
//定义⼀个函数指针
myPTypeFunc fp = NULL;
//通过函数指针类型 定义了⼀个函数指针 ,
//⽅法三:
//定义⼀个函数指针 变量
void (*myVarPFunc)(int a, int b);
3.4.6 函数重载总结
1) 重载函数在本质上是相互独立的不同函数。
2) 函数的函数类型是不同的
3) 函数返回值不能作为函数重载的依据
4) 函数重载是由函数名和参数列表决定的。
4.类和对象
4.1 基本概念
类是一种复杂的数据类型,它是将不同类型的数据和与这些树据相关的操作封装在一起的集合体。因此,类具有对数据的抽象性、隐藏性和封装性。
类是对现实世界中的客观事物的抽象,将具有相同属性的一类事物称作某个类。例如,将路上的各式各样的汽车抽象出它们的相同属性,称作汽车类。任何一种汽车都是属于汽车类中的一个实体,又称为一个实例,这便是对象。类是根据实际问题需要,有用户抽象出共同的属性后定义的一种类型,又称为自定义类型。
4.1.1 类与对象
4.1.2 成员变量和成员函数
面向对象的三大特点:封装、继承、多态。
4.2 封装和访问控制
4.2.1 从struct说起
当单一变量无法完成描述需求的时候,结构体类型解决了这一问题。可以将多个类型打包成一体。
这是C语言中封装的概念。
4.2.2 封装的访问属性
struct 中所有行为和属性都是 public 的(默认)。C++中的 class 可以指定行为和属性的访问方式。
封装,可以达到,对内开放数据,对外屏蔽数据,对外提供接口。达到了信息隐 蔽的功能。
比如我们用 struct 封装的类,即知其接口,又可以直接访问其内部数据,这样却没有达到信息隐蔽的功效。而 class 则提供了这样的功能,屏蔽内部数据,对外开放接口。
4.2.3 用class去封装带行为的类
class 封装的本质,在于将数据和行为,绑定在一起然后能通过对象来完成操作。
#include <iostream>
using namespace std:
class Data{
public:
void init(Data &d);
void print(Data &d);
bool isLeapYear(Data &d);
private:
int year;
int month;
int day;
};
void Data::init(Data &d)
{
cout <<"year month day:"<< endl;
cin >> d.year >> d.month >> d.day;
}
void Data::print(Data &d)
{
cout <<"year month day:"<< endl;
cout << d.year << ":" << d.month << ":" << d.day << endl;
}
bool Data::isLeapYear(Data &d)
{
if(((d.year % 4 == 0) && (d.year % 100 != 0))||((d.year % 400) == 0))
{
return true;
}
return false;
}
int main(void)
{
Data d;
d.init(d);
d.print(d);
if(d.isLeapYear(d))
{
cout << "leap year" << endl;
}else
{
cout << "not leap year" << endl;
}
return 0;
}
Date 类 访问自己的成员,可以不需要传引用的方式
封装有2层含义:
a.把属性和方法进行封装;
b.对属性和方法进行访问控制
Public修饰成员变量和成员函数可以在类的内部和类的外部被访问。
Private修饰成员变量和成员函数只能在类的内部被访问。
struct和class关键字区别 :
a.在用struct定义类时,所有成员的默认属性为public
b.在用class定义类时,所有成员的默认属性为private
4.3 面向对象编程案例
面向对象编程与面向过程编程
面向对象编程: 狗吃屎
面向过程编程:吃狗屎
4.3.1 求圆的周长和面积
#include <iostream>
using namespace std:
class Circle
{
private:
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;
}
};
int main(void)
{
Circle A;
A.Set_Radius(6.23);
cout << "A.Radius" << A.Get_Radius() << endl;
cout << "A.Get_Girth" << A.Get_Girth() << endl;
cout << "A.Get_Area" << A.Get_Area() << endl;
return 0;
}
4.3.2 程序设计方法发展历程
面向过程的结构化编程方法
--------------------
设计思路:自顶向下、逐步求精。采用模块分割与功能抽象,自顶向下、分而治之。
--------------------
程序结构:
按功能划分为若干个基本模块,形成一个树状结构。
各个模块间的关系尽可能简单,功能上相对独立;每个模块内部均是由顺序、选择、循环三种基本结构组成。
其模块化实现的方式是使用子程序。
---------------------
优点:
有效的将一个较复杂的程序系统设计任务分解成许多易于控制的和处理的子任务,便于开发与维护。
缺点:
可重用性差、数据安全性差、难以开发大型软件和图形界面应用程序
-----------------------
软件:
a) 把数据和处理数据的过程分离为相对独立的实体
b) 当数据结构改变时,所有相关的处理都要进行修改
c) 每一种相对于老问题的新方法都会带来额外的开销
d) 图形用户界面的应用程序,很难用过程来描述和实现,开发和维护都很困难。
------------------------
面向对象的基本概念
对象:
一般意义上的对象
a)是现实世界中一个实际存在的事物。
b)可以是有形的(比如一辆汽车),也可以是无形的(比如一项计划)
c) 是构成世界的一个独立单位,具有静态特征和动态特征
静态特征:可以用某种数据来᧿述
动态特征:对象所表现的行为或具有的功能
面向对象方法中的对象:
系统中用来描述客观事物的一个实体,它是用来构成系统的一个基本单位。对象由一组属性和一组行为构成。
属性:用来描述对象静态特征的数据项。
行为: 用来描述对象动态特征的操作序列。
类
分类--人类通常的思维方法
分类所依据的原则--抽象
分类的方法:
a) 忽略事物的非本质特征,只注意那些与当前目标有关的本质特征,从而找出事物的共性,把具有共同性质的事物划分为一类,得出一个抽象的概念。
b) 例如,石头、树木、汽车、房屋等都是人们在长期的生产和生活实践中抽象出的概念。
面向对象方法中的"类":
a) 具有相同属性和服务的一组对象的集合
b) 为属于该类的全部对象ᨀ供了抽象的᧿述,包括属性和行为两个主要部分。
类与对象的关系
犹如模具与铸件之间的关系,一个属于某类的对象称为该类的一个实例。
对象是类的实例化表现,类是对象的抽象描述。
封装:
也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
I. 把对象的属性和服务结合成一个独立的系统单元。
II.尽可能隐蔽对象的内部细节。对外形成一个边界(或者说一道屏障),只保留有限的对外接口使之与外部发生联系。
III.继承对于软件复用有着重要意义,是面向对象技术能够ᨀ高软件开发效率的重要原因之一。
IV.定义:特殊类的对象拥有其一般类的全部属性与服务,称作特殊类对一般类的继承。
V.例如:将轮船作为一个一般类,客轮便是一个特殊类。
多态:
多态是指在一般类中定义的属性或行为,被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或行为在一般类及其各个特殊类中具有不同的语义。
面向对象的软件工程:
面向对象的软件工程是面向对象方法在软件工程领域的全面应用。
它包括:
面向对象的分析(OOA)
面向对象的设计(OOD)
面向对象的编程(OOP)
面向对象的测试(OOT)
面向对象的软件维护(OOSM)
总结:
面向过程程序设计:数据结构 + 算法
主要解决科学计算问题,用户需求简单而固定。
特点
分析解决问题所需要的步骤
利用函数实现各个步骤
依次调用函数解决问题
问题
软件可重用性差
软件可维护性差
构建的软件无法满足用户需求
面向对象程序设计:由现实世界建立软件模型
将现实世界中的事物直接映射到程序中,可直接满足用户需求
特点
直接分析用户需求中涉及的各个实体
在代码中描述现实世界中的实体
在代码中关联各个实体协同工作解决问题
优势
构建的软件能够适应用户需求的不断变化
直接利用面向过程方法的优势而避开其劣势
4.3.3 C语言和C++语言的关系
C语言 + 面向对象方法论 ===> Objective C /C++
C语言和C++并不是对立的竞争关系
C++是C语言的加强,是一种更好的C语言
C++是以C语言为基础的,并且完全兼容C语言的特性
学习C++并不会影响原有的C语言知识,相反会根据加深对C的认知;
学习C++可以接触到更多的软件设计方法,并带来更多的机会。
4.4 对象的构造和析构
4.4.1 如果没有构造函数?
面向对象的思想是从生活中来,手机、车出厂时,是一样的。
生活中存在的对象都是被初始化后才上市的;
初始状态是对象普遍存在的一个状态的。
如果不用构造函数初始化,该怎么办:
为每个类都提供一个public的initialize函数;
对象创建后立即调用initialize函数进行初始化。
缺点:
a) initialize只是一个普通的函数,必须显示的调用 ;
b) 一旦由于失误的原因,对象没有初始化,那么结果将是不确定的;
c) 没有初始化的对象,其内部成员变量的值是不定的。
针对以上缺点,C++对类提供了一个给对象的初始化方案,就是构造函数。
4.4.2 构造函数
定义
C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数。
构造函数的调用:
自动调用:一般情况下C++编译器会自动调用构造函数。
手动调用: 在一些情况下则需要手工调用构造函数。
规则:
a) 在对象创建时自动调用,完成初始化相关工作。
b) 无返回值,与类名同,默认无参,可以重载,可默认参数。
c) 一经实现,默认不复存在。
class A
{
A(形参)
{
}
};
4.4.3 析构函数
定义
C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数。
形式如下:
class A{
~A()
{
}
};
规则:
1 对象销毁时,自动调用。完成销毁的善后工作;
2 无返值 ,与类名同。无参。不可以重载与默认参数 。
析构函数的作用,并不是删除对象,而在对象销毁前完成的一些清理工作。
4.4.4 构造函数的分类及调用
class Test
{
public:
Test() //无参构造函数
{
;
}
Test(int a,int b) //带参构造函数
{
;
}
Test(const Test &obj) //赋值构造函数
{
;
}
private:
int a;
int b;
};
1)无参构造函数
#include <iostream>
using namespace std;
class Test
{
private:
int a;
int b;
public:
Test()
{
a = 0;
b = 0;
cout << "a = " << a << endl;
cout << "\r\n" << endl;
cout << "b = " << b << endl;
}
};
int main(void)
{
Test t;
return 0;
}
- 有参构造函数
#include <iostream>
using namespace std;
class Test
{
private:
int a;
int b;
public:
Test(int a)
{
this -> a = a;
}
Test(int a,int b)
{
this -> a = a;
this -> b = b;
}
};
int main(void)
{
Test t1(10);
Test t2(10,20);
return 0;
}
-
拷贝构造函数
由己存在的对象,创建新对象。 也就是说新对象,不由构造器来构造,而是由拷贝 构造器来完成。 拷贝构造器的格式是固定的。
class A
{
A(const A &another)
{
}
};
使用拷贝构造函数的几种应用场景
#include <iostream>
using namespace std;
class Test
{
private:
int m_a;
public:
Test()
{
cout<<"我是⽆参构造函数,被调⽤了"<<endl;
}
Test(int a)
{
m_a = a;
}
Test(const Test &another_obj)
{
cout<<"我也是构造函数,我是通过另外⼀个对象, 来初始化我⾃⼰"<<endl;
m_a = another_obj.m_a;
}
~Test()
{
cout<<"我是析构函数,⾃动被调⽤了"<<endl;
}
void printT()
{
cout<<"m_a="<<m_a<<endl;
}
};
//------------------------------1------------------------------
int main(void)
{
Test t1(10);
Test t2 = t1; //⽤对象t1 初始化 对象 t2
t2.printT();
return 0;
}
//------------------------------2------------------------------
int main(void)
{
Test t1(10);
Test t2(t1); //⽤对象t1 初始化 对象 t2
t2.printT();
return 0;
}
#include <iostream>
using namespace std;
class Location
{
private:
int X;
int Y;
public:
//带参构造函数
Location(int X,int Y)
{
this->X=X;
this->Y=Y;
cout<<"Constructor Object."<<endl;
}
//copy构造函数 完成对象的初始化
Location(const Location &obj)
{
X = obj.X;
Y = obj.Y;
cout<<"Constructor Object."<<endl;
}
~Location()
{
cout<<X<<","<<Y<<"Object destroyed."<<endl;
}
int GetX()
{
return X;
}
int GetY()
{
return Y;
}
};
//会执⾏ p = b 的操作,p会调⽤copy构造函数进⾏初始化
void func(Location p)
{
cout<<"func begin"<<endl;
cout<<p.GetX()<<endl;
cout<<"func end"<<endl;
}
void test()
{
Location a(1, 2);
//对象a 调⽤带参数的构造函数进⾏初始化
func(b);
//b实参取初始化形参p,会调⽤copy构造函数
}
int main(void)
{
test();
return 0;
}
(5) 默认构造函数
二个特殊的构造函数
1) 默认无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
2) 默认拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制。
4.4.4 构造函数规则
规则:
1 系统提供默认的拷贝构造器。一经实现,不复存在。
2 系统提供的时等位拷贝,也就是所谓的浅浅的拷贝。
3 要实现深拷贝,必须要自定义。
#incldue <iostream>
using namespace std;
//当类中定义了拷⻉构造函数时,c++编译器不会提供⽆参数构造函数
//当类中定义了有参数构造函数是,c++编译器不会提供⽆参数构造函数
//在定义类时, 只要你写了构造函数,则必须要⽤
class Test
{
private:
int a;
int b;
public:
Test(const Test &obj)
{
a = obj.a + 100;
b = obj.b + 100;
}
void printT()
{
cout << "a:" << a << "b:" << b << endl;
}
};
int main(void)
{
Test t1; //error, 没有合适的构造函数
return 0;
}
4.4.6 浅拷贝与深拷贝
系统提供默认的拷贝构造器,一经定义不再提供。
但系统提供的默认拷贝构造器是 等位拷贝,也就是通常意义上的浅拷贝。
如果类中包含的数据元素全部在栈上,浅拷贝 也可以满足需求的。
但如果堆上的数据,则会发生多次析构行为。
4.4.7 构造函数初始化列表
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,
如果没有初始化列表,那么他将无法完成第一步,就会报错。
#incldue <iostream>
using namespace std;
class ABC
{
private:
int a;
int b;
int c;
public:
ABC(int a,int b,int c)
{
this->a = a;
this->b = b;
this->c = c;
}
~ABC()
{
printf("a = %d,b = %d,c = %d\r\n",a,b,c);
printf("~ABC() ..\n");
}
};
class MyD
{
private:
ABC abc1;
ABC abc2;
const int m;
public:
MyD():abc1(1,2,3),abc2(4,5,6),m(100)
{
cout<<"MyD()"<<endl;
}
~MyD()
{
cout<<"~MyD()"<<endl;
}
};
int main()
{
MyD myD;
return 0;
}
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
出始化列表中的初始化顺序,与声明顺序有关,与前后赋值顺序无关。
4.5 对象动态建立和释放new和delete
在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和撤销内存空间的。
C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。
new和delete是运算符,不是函数,因此执行效率高。
虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。
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
用new分配数组空间时不能指定初值。
如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。
malloc不会调用类的构造函数,而new会调用类的构造函数。
Free不会调用类的析构函数,而delete会调用类的析构函数。
4.6 静态成员变量和成员函数
在 C++中,静态成员是【属于整个类】的而不是某个对象,【静态成员变量只存储一份】供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
类的静态成员,属于类,也属于对象,但终归属于类。
4.6.1 静态成员变量
//声明
static 数据类型 成员变量; //在类的内部
//初始化
数据类型 类名::静态数据成员 = 初值; //在类的外部
//调⽤
类名::静态数据成员
类对象.静态数据成员
#include <iostream>
using namespace std;
class Box
{
public:
static int height;
int length;
int width;
Box(int l,int w):length,width(w)
{
}
int volume()
{
return length*width*height;
}
};
int Box::height = 5;
int main(void)
{
cout<<Box::height<<endl;
Box b(1,1);
cout<<b.height<<endl;
cout<<b.volume()<<endl;
return 0;
}
1,static 成员变量实现了同类对象间信息共享。
2,static 成员类外存储,求类大小,并不包含在内。
3,static 成员是命名空间属于类的全局变量,存储在 data 区。
4,static 成员只能类外初始化。
5,可以通过类名访问(无对象生成时亦可),也可以通过对象访问。
4.6.2 静态成员函数
//声明
static 函数声明
//调⽤
类名::函数调⽤
类对象.函数调⽤
#include <iostream>
using namespace std;
class Student
{
private:
int num;
int age;
float score;
static float sum;
static int count;
public:
Student(int n,int a,float s):num(n),age(a),score(s){}
void total()
{
count++;
sum += score;
}
static float average();
};
float Student::sum = 0;
int Student::count = 0;
float Student::average()
{
return sum/count;
}
int main(void)
{
Student stu[3]= {
Student(1001,14,70),
Student(1002,15,34),
Student(1003,16,90)
};
for(i = 0;i < 3;i++)
{
stu[i].total();
}
cout<<Student::average()<<endl;
return 0;
}
1、静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员, 完成对静态数据成员的封装。
2、静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时this 指针被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。
4.7 编译器对属性和方法的处理机制
4.7.1 静态成员占多大
4.7.2 静态成员占多大
a) C++类对象中的成员变量和成员函数是分开存储的
成员变量: 普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式 。
静态成员变量:存储于全局数据区中成员函数:存储于代码段中。
1、C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效!
2、C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。
3、静态成员函数、成员变量属于类
4、静态成员函数与普通成员函数的区别
静态成员函数不包含指向具体对象的指针
普通成员函数包含一个指向具体对象的指针
4.7.3 this指针
1) 若类成员的函数的形参和类的属性名字相同,通过this指针来解决。
2) 类的成员函数可通过const修饰。
4.7.4 全局函数与成员函数
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;
}
#incldue <iostream>
using namespace std;
class test
{
private:
int a;
int b;
public:
test(int a,int b)
{
this->a = a;
this->b = b;
}
test()
{
}
test testAdd(test &t2)
{
test temp(this ->a+t2.a,this->b+t2.b);
return temp;
}
test & testAdd2(test &t2)
{
this->a += t2.a;
this->b += t2.b;
return *this;
}
void print()
{
cout << "a: " << a << " b: " << b << endl;
}
};
test testAdd(test &t1, test &t2)
{
test t3;
return t3;
}
void main()
{
test t1(1, 2);
test t2(3, 4);
test t3, t4;
// 全局函数的方法
t3 = testAdd(t1, t2);
// 成员变量的方法
t4 = t1.testAdd(t2); // 匿名对象 复制给 t4
test t5 = t1.testAdd(t2);//匿名对象,直接转换为 t5
t4.print();
t5.print();
system("pause");
return;
}
4.8 友元
采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的通信接口。
但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。
除了友元函数外,还有友元类,两者统称为友元。
友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
友元可以是一个函数,该函数被称为友元函数;
友元也可以是一个类,该类被称为友元类。
4.8.1 友元函数
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函 数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上 关键字 friend,其格式如下:
friend 类型 函数名(形式参数);
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
全局函数作为友元函数
#include<iostream>
#include<cmath>
using namespace std;
class Point
{
private:
double x,y;
public:
Point(double xx,double yy)
{
x = xx;
y = yy;
}
void Getxy();
friend double Distance(Point &a,Point &b);
};
void Point::Getxy()
{
cout <<"("<<x<<","<<y<<")"<<endl;
}
double Distance(Point &a,Point &b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx*dx + dy*dy);
}
int main(void)
{
Point p1(3.0,4.0);
Point p2(6.0,8.0);
p1.Getxy();
p2.Getxy();
double d = Distance(p1,p2);
cout <<"Distance is"<<d<<endl;
return 0;
}
类成员函数作为友元函数
#include<iostream>
#include<cmath>
using namespace std;
class Point;
//前向声明,是⼀种不完全型声明,即只需提供类名(⽆需提供类实现)即可。仅可⽤于声明指针和引⽤。
class ManagerPoint
{
public:
double Distance(Point &a, Point &b);
};
class Point
{
private:
double x,y;
public:
Point(double xx,double yy)
{
x = xx;
y = yy;
}
void Getxy();
friend double ManagerPoint :: Distance(Point &a,Point &b);
};
void Point::Getxy()
{
cout <<"("<<x<<","<<y<<")"<<endl;
}
double ManagerPoint :: Distance(Point &a,Point &b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx*dx + dy*dy);
}
int main(void)
{
Point p1(3.0,4.0);
Point p2(6.0,8.0);
p1.Getxy();
p2.Getxy();
double d = Distance(p1,p2);
cout <<"Distance is"<<d<<endl;
return 0;
}
4.8.2 友元对象
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
其中:friend 和class是关键字,类名必须是程序中的⼀个已定义过的类。
例如,以下语句说明类 B 是类 A 的友元类:
class A
{
public:
friend class B;
};
经过以上说明后,类 B 的所有成员函数都是类A的友元函数,能存取类A的私有成员和保护成员。
class A {
public:
inline void Test()
{
}
private:
int x ,y;
friend Class B;
};
class B
{
public:
inline void Test()
{
A a;
printf("x = %d,y = %d\r\n",a.x,a.y);
}
};
4.8.3 论有元
注意事项
a) 友元关系不能被继承。
b) 友元关系是单向的,不具有交换性。若类B是类A 的友元,类A 不一定是类B的友元,要看在类中是否有相应的声明。
c) 友元关系不具有传递性。若类 B 是类A的友元,类C是B的友元,类C不一定 是类A的友元,同样要看类中是否有相应的声明。
4.9 运算符重载(一种运算符实现多种用途)
所谓重载,就是重新赋予新的含义。
函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是”一名多用”。
运算符也可以重载。实际上,我们已经在不知不觉之中使用了运算符重载。例如,大家都已习惯于用加法运算符”+”对整数、单精度数和双精度数进行加法运算,如5+8, 5.8 +3.67等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的, 但由于C++已经对运算符”+”进行了重载,所以就能适用于int, float, doUble类型的运算。
又如”<<“是C++的位运算中的位移运算符(左移),但在输出操作中又是与流对象cout配合使用的【流插入运算符】,”>>“也是位移运算符(右移),但在输入操作中又是与流对象cin配合使用的【流提取运算符】。这就是运算符重载(operator overloading)。
C++系统对”<<“和”>>“进行了重载,用户在不同的场合下使用它们时,作用是不同的。对”<<“和”>>“的重载处理是放在头文件iostream中的。因此,如果要在程序中用”<< “和”>>”作流插入运算符和流提取运算符,必须在本文件模块中包含头文件iostream(当然还应当包括”using namespace std“)。
现在要讨论的问题是:用户能否根据自己的需要对C++已提供的运算符进行重载,赋予它们新的含义,使之一名多用。
运算符重载的本质是函数重载。
重载函数的一般格式如下:
函数类型 operator 运算符名称(形参表列)
{
重载实体;
}
operator 运算符名称 在一起构成了新的函数名。比如:
const Complex operator+(const Complex &c1,const Complex &c2);
我们会说,operator+ 重载了重载了运算符+。
4.9.1 友元重载
#include <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y = 0):_x(x),y_(y){}
void dis()
{
cout << "(" << _x << "," << _y << ")" << endl;
}
friend const Complex operator + (const Complex &c1,const Complex &c2);
};
const Complex operator + (const Complex &c1,const Complex &c2)
{
return Complex(c1._x + c2._x,c1._y+c2._y);
}
int main(void)
{
Complex c1(2,3);
Complex c2(3,4);
c1.dis();
c2.dis();
Complex c3 = c1 + c2;
//Complex c3=operator+(c1,c2);
c3.dis();
return 0;
}
4.9.2 成员重载
#incldue <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y = 0):_x(x),y_(y){}
void dis()
{
cout << "(" << _x << "," << _y << ")" << endl;
}
friend const Complex operator + (const Complex &c1,const Complex &c2);
const Complex operator+(const Complex &another);
};
const Complex operator + (const Complex &c1,const Complex &c2)
{
cout << "友元函数重载" << endl;
return Complex(c1._x + c2._x,c1._y+c2._y);
}
const Complex Complex::operator+(const Complex &another)
{
cout << "成员函数重载" << endl;
return Complex(this->_x + another._x,this->_y+another._y);
}
int main(void)
{
Complex c1(2,3);
Complex c2(3,4);
c1.dis();
c2.dis();
//Complex c3 = c1 + c2;
//Complex c3 = operator + (c1,c2);
Complex c3 = c1 + c2;
c3.dis();
return 0;
}
#inclcue <iostream>
using namespace std;
class complex
{
private:
double real;
double imag;
public:
complex()
{real=imag=0.0;}
complex(double r)
{real=r;imag=0.0;}
complex(double r,double i)
{real=r;imag=i;}
complex operator + (const complex &c);
complex operator - (const complex &c);
complex operator * (const complex &c);
complex operator / (const complex &c);
friend void print(const complex &c);
};
inline complex complex::operator + (const complex &c)
{
return complex(real+c.real,imag+c.imag);
}
inline complex complex::operator - (const complex &c)
{
return complex(real-c.real,imag-c.imag);
}
inline complex complex::operator * (const complex &c)
{
return complex(real*c.real-imag*c.imag,real*c.imag+imag*c.real);
}
inline complex complex::operator / (const complex &c)
{
return complex((real*c.real+imag*c.imag)/(c.real*c.real+c.imag*c.imag),
(imag*c.real-real*c.imag)/(c.real*c.real+c.imag*c.imag));
}
void print(const complex &c)
{
if(c.imag < 0)
{
cout << c.real << c.imag << "i";
}else
{
cout << c.real << "+" << c.imag << "i";
}
}
int main(void)
{
complex c1(2.0),c2(3.0,-1.0),c3;
c3 = c1+c2;
cout << "\n c1 + c2";
print(c3);
c3 = c1-c2;
cout << "\n c1 - c2";
print(c3);
c3 = c1*c2;
cout << "\n c1 * c2";
print(c3);
c3 = c1/c2;
cout << "\n c1 / c2";
print(c3);
c3 = (c1+c2)*(c1-c2)*c2/c1;
cout<<"\n (c1+c2)*(c1-c2)*c2/c1";
print(c3);
cout << endl;
return 0;
}
4.10.2 重载规则
1)C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载。
例如,有人觉得 BASIC 中用“* *”作为幂运算符很方便,也想在 C++中将“* *”定义为幂运算符,用“3* *5”表示 35,这是不行的。
2)C++允许重载的运算符
C++中绝大部分运算符都是可以被重载的。
不能重载的运算符只有 4 个:
前两个运算符不能重载是为了保证访问成员的功能不能被改变,域运算符合sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。
3)重载不能改变运算符运算对象(即操作数)的个数
如,关系运算符“>”和“<”等是双目运算符,重载后仍为双目运算符,需要两个参数。运算符”+“,”-“,”*“,”&“等既可以作为单目运算符,也可以作为双目运算符,可以分别将它们重载为单目运算符或双目运算符。
4)重载不能改变运算符的优先级别
例如”*“和”/“优先级高于”+“和”-“,不论怎样进行重载,各运算符之间的优先级不会改变。有时在程序中希望改变某运算符的优先级,也只能使用加括号的方法 强制改变重载运算符的运算顺序。
5)重载不能改变运算符的结合性
如,复制运算符”=“是右结合性(自右至左),重载后仍为右结合性。
6)重载运算符的函数不能有默认的参数
否则就改变了运算符参数的个数,与前面第(3)点矛盾。
7)重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一 个是类对象(或类对象的引用)
也就是说,参数不能全部是 C++的标准类型,以防止用户修改用于标准类型数据成 员的运算符的性质。
如下面这样是不对的:复制代码 代码如下:
int operator + (int a,int b) { return(a-b); }
原来运算符+的作用是对两个数相加,现在企图通过重载使它的作用改为两个数相 减。如果允许这样重载的话,如果有表达式 4+3,它的结果是 7 还是 1 呢?显然,这是 绝对要禁止的。
8)用于类对象的运算符一般必须重载,但有两个例外,运算符”=“和运算符”&“不 必用户重载。
复制运算符”=“可以用于每一个类对象,可以用它在同类对象之间相互赋值。因 为系统已为每一个新声明的类重载了一个赋值运算符,它的作用是逐个复制类中的数据 成员地址运算符&也不必重载,它能返回类对象在内存中的起始地址。
9) 应当使重载运算符的功能类似于该运算符作用于标准类型数据时候时所实现的功能
例如,我们会去重载”+“以实现对象的相加,而不会去重载”+“以实现对象相 减的功能,因为这样不符合我们对”+“原来的认知。
10) 运算符重载函数可以是类的成员函数,也可以是类的友元函数,还可以是既非类 的成员函数也不是友元函数的普通函数。
4.10.3 双目运算符重载
//使⽤: L#R
operator#(L,R); //全局函数
L.operator#(R); //成员函数
#include <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y=0):_x(x),_y(y){}
void dis()
{
cout << "(" << _x << "," << _y << ")" << endl;
}
Complex &operator+=(const Complex &c)
{
this->_x+=c._x;
this->_y+=c._y;
return *this;
}
};
int main(void)
{
int a=10,b=20,c=30;
(a += b) += c;
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
Complex a1(10,0),b1(20,0),c1(30,0);
// 此时重载函数+=返回的是 Complex &
// ⼀定要注意在连等式中,返回引⽤和返回对象的区别
(a1 += b1) += c1;
a1.dis();
b1.dis();
c1.dis();
return 0;
}
friend Complex &operator-=(Complex &c1,const Complex &c2)
{
}
4.10.4 单目运算符重载
//使⽤: #M 或者 M#
operator#(M); //全局函数
M.operator#() //成员函数
#include <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y=0):_x(x),_y(y){}
void dis()
{
cout<<"("<<_x<<","<<_y<<")"<<endl;
}
friend Complex &operator++(Complex &c);
Complex &operator++(Complex &c)
{
c._x++;
c._y++;
return c;
}
};
int main(void)
{
int n = 10;
cout<<n<<endl;//10
cout<<++n<<endl;//11
cout<<n<<endl;//11
cout<<++++n<<endl;//13
cout<<n<<endl;
Complex c(10,10);
c.dis();//10 10
Complex c2=++c;
c2.dis(); //11 11
c.dis(); //11 11
c2 = ++++c;
c2.dis(); //13 13
c.dis(); //13 13
return 0;
}
后加加
#include <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y=0):_x(x),_y(y){}
void dis()
{
cout<<"("<<_x<<","<<_y<<")"<<endl;
}
friend const Complex operator++(Complex &c,int);
};
const Complex operator++(Complex &c,int)
{
Complex t(c._x,c._y);
c._x++;
c._y++;
return t;
}
int main(void)
{
int n = 10;
cout<<n<<endl; //10
cout<<n++<<endl; //10
cout<<n<<endl; //11
Complex c(10);
c.dis();
Complex c2=c++;
c2.dis();
c.dis();
return 0;
}
4.10.5 输入输出算符重载
istream & operator>>(istream &,⾃定义类&);
ostream & operator<<(ostream &,⾃定义类&);
通过友元来实现,避免修改 c++的标准库。
#include <iostream>
using namespace std;
class Complex
{
private:
float _x;
float _y;
public:
Complex(float x=0,float y=0):_x(x),_y(y){}
void dis()
{
cout<<"("<<_x<<","<<_y<<")"<<endl;
}
friend ostream &operator<<(ostream &os,const Complex &c);
friend istream &operator>>(istream &is,Complex &c);
};
ostream &operator<<(ostream &os,const Complex &c)
{
os<<"("<<c._x<<","<<c._y<<")";
return os;
}
istream &operator>>(istream &is,Complex &c)
{
is>>c._x>>c._y;
return is;
}
int main()
{
Complex c(2,3);
cout<<c<<endl;
cin>>c;
cout<<c<<endl;
return 0;
}
4.10.6 友元还是成员
假设,我们有类 Sender 类和 Mail 类,实现发送邮件的功能。
sender<< mail;
sender 左操作数,决定了 operator<<为 Sender 的成员函数,而 mail 决定了 operator<<要作 Mail 类的友员。
#incldue <iostream>
using namespace std;
class Mail;
class Sender
{
private:
string _addr;
public:
Sender(string s):_addr(s){}
Sender& operator<<(const Mail &mail); //成员
};
class Mail
{
private:
string _title;
string _content;
public:
Mail(string _t,string _c ):_title(_t),_content(_c){}
friend Sender& Sender::operator<<(const Mail &mail);
};
Sender& Sender::operator<<(const Mail &mail)
{
cout<<"Address:"<<_addr<<endl;
cout<<"Title :"<<mail._title<<endl;
cout<<"Content:"<<mail._content<<endl;
return *this;
}
int main(void)
{
Sender sender("danbing_at@gmail.com");
Mail mail("note","meeting at 3:00 pm");
Mail mail2("tour","One night in beijing");
sender<<mail<<mail2;
return 0;
}
结论:
1) 一个操作符的左右操作数不一定是相同类型的对象,这就涉及到将该操作符函数定义为谁的友元,谁的成员问题。
2) 一个操作符函数,被声明为哪个类的成员,取决于该函数的调用对象(通常是左操作数)。
3) 一个操作符函数,被声明为哪个类的友员,取决于该函数的参数对象(通常是右操作数)。
4.10.7 运算符重载提高
A)赋值运算符重载 (operator=)
用一个己有对象,给另外一个己有对象赋值。两个对象均己创建结束后,发生的赋 值行为。
类名
{
类名& operator=(const 类名& 源对象) 拷⻉体
};
class A
{
A& operator=(const A& another)
{
return *this;
}
};
规则:
1)系统提供默认的赋值运算符重载,一经实现,不复存在。
2)系统提供的也是等位拷贝,也就浅拷贝,一个内存泄漏,重析构。
3)要实再深深的赋值,必须自定义。
4)自定义面临的问题有三个:
4.1 自赋值
4.2 内存泄漏
4.3 重析构
5)返回引用,且不能用 const 修饰。其目的是实现连等式。
B)数组下标运算符 (operator[])
类型 类 :: operator[] (类型);
设 x 是类 X 的一个对象,则表达式
x[y]
可被解释为 x.operator[] (y)
C)堆内存操作符 (operator new/delete)
适用于极个别情况需要定制的时候才用的到。一定很少用。
D)函数调用符号 (operator () )
把类对象像函数名一样使用。
仿函数(functor),就是使一个类的使用看上去象一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
class 类名 {
返值类型 operator()(参数类型) 函数体
}
5.继承和派生
在 C++中可重用性(software reusability)是通过继承(inheritance)这一机制来实现的。 如果没有掌握继承性,就没有掌握类与对象的精华。
5.1 类和类之间的关系
has-A,uses-A 和 is-A
has-A 包含关系,用以描述一个类由多个“部件类”构成。实现has-A关系用类成员表示,即一个类中的数据成员是另一种已经定义的类。
uses-A 一个类部分地使用另一个类。通过类之间成员函数的相互联系,定义友员或对象参数传递实现。
is-A 机制称为“继承”。关系具有传递性,不具有对称性。
定义
类的继承,是新的类从已有类那里得到已有的特性。或从已有类产生新类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。
派生与继承,是同一种意义两种称谓。 isA 的关系。
派生类的组成
派生类中的成员,包含两大部分,一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。
几点说明:
1、全盘接收,除了构造器与析构器。基类有可能会造成派生类的成员冗余,所以说基类是需设计的。
2、派生类有了自己的个性,使派生类有了意义。
5.2 继承的方式
5.2.1 语法
class 派⽣类名:[继承⽅式] 基类名
{
派⽣类成员声明;
};
一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类, 称为单继承。
下面从单继承讲起。
5.2.2 protected 访问控制
protected 对于外界访问属性来说,等同于私有,但可以派生类中可见。
5.2.3 派生类成员的标识和访问
1、需要被外界访问的成员直接设置为public ;
2、只能在当前类中访问的成员设置为private;
3、只能在当前类和子类中访问的成员设置为protected,protected成员的访问权限介于public和private之间。
5.3 继承中的构造和析构
5.3.1 类型兼容性原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。
这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
类型兼容规则中所指的替代包括以下情况:
子类对象可以当作父类对象使用;
子类对象可以直接赋值给父类对象;
子类对象可以直接初始化父类对象;
子类指针可以直接指向子类对象;
父类引用可以直接引用子类对象;
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。
子类就是特殊的父类 (base *p = &child;)
#include <iostream>
using namespace std;
class Parent
{
public:
void printP()
{
cout<<"parent...."<<endl;
}
};
class Child:public Parent
{
public:
void printC()
{
cout<<"child..."<<endl;
}
};
void print01(Parent *p)
{
p->printP();
}
void print02(Parent &p)
{
p.printP();
}
int main(void)
{
Child c1;
c1.printC();
Parent *p = NULL;
//可以⽤⽗类指针 指向 ⼦类对象
p = &c1;
p->printP(); //执⾏⽗类的函数
Child c2;
Parent p2;
print01(&p2);
print01(&c2); //⽗类指针指向⼦类对象
print02(p2);
print02(c2); //⽗类引⽤指向⼦类对象
//第⼆层含义 ⽤⼦类初始化⽗类对象
Child c3;
Parent p3 = c3;
return 0;
}
5.3.2 继承中的对象模型
类在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员得到的。
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化。
在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理。
#include <iostream>
using namespace std;
class Parent
{
private:
const char *s;
public:
Parent(const char *s)
{
this->s = s;
cout<<"Parent()"<<" "<<s<<endl;
}
~Parent()
{
cout<<"~Parent()"<<endl;
}
};
class Child:public Parent
{
private:
int a;
public:
Child(int a):Parent("Parameter from Child!")
{
cout<<"Child()"<<endl;
this->a = a;
}
Child(int a,const char *s):Parent(s)
{
cout<<"Child()"<<endl;
this->a = a;
}
~Child()
{
cout<<"~Child()"<<endl;
}
};
void run()
{
Child child(10,"Parameter form child...");
}
int main(void)
{
run();
return 0;
}
5.3.3 继承中构造析构调用原则
a.子类对象在创建时会首先调用父类的构造函数;
b.父类构造函数执行结束后,执行子类的构造函数;
c.当父类的构造函数有参数时,需要在子类的初始化列表中显示调用;
d.析构函数调用的先后顺序与构造函数相反。
5.3.4 继承和组合并存,构造和析构原则
a.先构造父类,再构造成员变量、最后构造自己;
b.先析构自己,在析构成员变量、最后析构父类。
#include <iostream>
using namespace std;
class Object
{
public:
Object(const char *)
{
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
{
private:
Object o1;
Object o2;
public:
Child():o2("o2"),o1("o1"),Parent("Parameter from Child!")
{
cout<<"Child()"<<endl;
}
~Child()
{
cout<<"~Child()"<<endl;
}
};
void run()
{
Child child;
}
int main(void)
{
run();
return 0;
}
5.3.5 继承中同名成员变量处理方法
a) 当子类成员变量与父类成员变量同名时,子类依然从父类继承同名成员;
b) 在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符;
c) 同名成员存储在内存中的不同位置 ;
同名成员变量和成员函数通过作用域分辨符进行区分
5.3.6 派生类中的static关键字
a) 基类定义的静态成员,将被所有派生类共享;
b) 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质 (遵守派生类的访问控制);
c) 派生类中访问静态成员,用以下形式显式说明:
类名::成员
或通过对象访问:对象名.成员
static函数也遵守3个访问原则;
static易犯错误(不但要初始化,更重要的显示的告诉编译器分配内存);
5.4 多继承
俗话讲的,鱼与熊掌不可兼得,而在计算机就可以实现,生成一种新的对象,叫熊掌鱼,多继承自鱼和熊掌即可。还比如生活中,“兼”。
5.4.1 语法
派⽣类名::派⽣类名(参数总表):基类名 1(参数表 1),基类名(参数名 2)....
基类名 n(参数名 n),内嵌⼦对象 1(参数表 1),
内嵌⼦对象 2(参数表 2).... 内嵌⼦对象 n(参数表 n)
{
派⽣类新增成员的初始化语句;
}
一个类有多个直接基类的继承关系称为多继承
5.4.2 沙发床实现
//bed.h
#ifndef BED_H
#define BED_H
//床类
class Bed
{
public:
Bed();
~Bed();
void sleep();
};
#endif // BED_H
//bed.cpp
#include "bed.h"
#include <iostream>
using namespace std;
Bed::Bed()
{
}
Bed::~Bed()
{
}
void Bed::sleep()
{
cout<<"take a good sleep"<<endl;
}
//sofa.h
#ifndef SOFA_H
#define SOFA_H
//沙发类
class Sofa{
public:
Sofa();
~Sofa();
void sit();
};
#endif // SOFA_H
#include "sofa.h"
#include <iostream>
using namespace std;
Sofa::Sofa()
{
}
Sofa::~Sofa()
{
}
void Sofa::sit()
{
cout<<"take a rest"<<endl;
}
//main.cpp
#include <iostream>
#include "sofa.h"
#include "bed.h"
using namespace std;
class SofaBed:public Sofa,public Bed
{
private:
char Factury256[256];
public:
void SofaBed(void)
{
cout << "My name is SoftBed" << end;
}
~void SofaBed(void)
{
cout << "SoftBed Is Die" << endl;
}
}
int main(void)
{
Sofa s;
Bed b;
b.sleep();
s.sit();
SofaBed sb;
sb.sit();
sb.sleep();
return 0;
}
5.5 虚继承
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
5.5.1 多继承中二义性问题
分析如下:
5.5.2 虚继承virtual
a) 如果一个派生类从多个基类派生,而这些基类又有一个共同 的基类,则在对该基类中声明的名字进行访问时,可能产生二义性;
b) 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处 汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象;
c) 要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类;
d) 虚继承声明使用关键字virtual 。
6.多态
6.1 什么是多态
6.1.1 浅析多态的意义
C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统 升级,维护,调试的工作量和复杂度。
举个例子,人类用手吃饭是本能,英国人用刀叉吃饭,中国人用筷子吃饭,我们问“这个人是怎么吃饭的?”,应该根据其国别来回答,而不是简单的说用手吃。这就是多态。
#include <iostream>
using namespace std;
class Human
{
public:
virtual void eating(void)
{
cout << "use hand to eating !" << endl;
}
};
class Englishman:public Human
{
void eating(void)
{
cout << "use knife to eating !" << endl;
}
};
class Chinese:public Human
{
void eating(void)
{
cout << "use chopsticks to eating !" << endl;
}
}
void test_eating(Human &h)
{
h.eating();
}
int main(int argc,char **argv)
{
Human h;
Englishman e;
Chinese c;
test_eating(h);
test_eating(e);
test_eating(c);
return 0;
}
6.1.2 赋值兼容(多态)
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
赋值兼容是一种默认行为,不需要任何的显示的转化步骤。
赋值兼容规则中所指的替代包括以下的情况:
1.派生类的对象可以赋值给基类对象;
2.派生类的对象可以初始化基类的引用;
3.派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a)
{
this->a = a;
cout<<"Parent a"<<a<<endl;
}
void print() //⼦类的和⽗类的函数名字⼀样
{
cout<<"Parent 打印 a:"<<a<<endl;
}
private:
int a;
};
class Child:public Parent
{
private:
int b;
public:
Child(int b):Parent(10)
{
this->b =b;
cout<<"Child b"<<b<<endl;
}
void print()
{
cout<<"Child 打印 b:"<<b<<endl;
}
}
void howToPrint(Parent *base)
{
base->print(); //⼀种调⽤语句 有多种表现形态...
}
void howToPrint2(Parent &base)
{
base.print();
}
int main(void)
{
Parent *base = NULL;
Parent p1(20);
Child c1(30);
base = &p1;
base->print(); //执⾏⽗类的打印函数
base = &c1;
/*
编译器认为最安全的做法是编译到⽗类的print函数,因为⽗类和⼦类肯定都有相同的print函数。 */
base->print(); //执⾏谁的函数 ? //貌似我们希望之星Child的print函数
Parent &base2 = p1;
base2.print(); //执⾏⽗类的打印函数
Parent &base3 = c1;
base3.print(); //执⾏谁的函数?
//函数调⽤
howToPrint(&p1);
howToPrint(&c1);
howToPrint2(p1);
howToPrint2(c1);
return 0;
}
6.1.3 面向对象新需求
编译器的做法不是我们期望的;
根据实际的对象类型来判断重写函数的调用;
如果父类指针指向的是父类对象则调用父类中定义的函数;
如果父类指针指向的是子类对象则调用子类中定义的重写函数;
6.1.4 解决方案
1)C++中通过virtual关键字对多态进行支持;
2)使用virtual声明的函数被重写后即可展现多态特性。
#include <iostream>
using namespace std;
class HeroFighter
{
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;
}
};
//如果把这个结构放在动态库⾥⾯
void objPK(HeroFighter *hf,enemyFighter *enemyF)
{
if(hf->ackPower() >enemyF->destoryPower())
{
printf("英雄打败敌⼈。。。胜利\n");
}else
{
printf("英雄。。。牺牲\n");
}
}
int main(int argc,char **argv)
{
HeroFighter hf;
enemyFighter ef;
objPK(&hf,&ef);
AdvHeroFighter advhf;
objPK(&advhf,&ef);
return 0;
}
6.1.5 多态的工程意义
封装:突破了C语言函数的概念。
继承:代码复用,复用原来写好的代码。
多态:多态可以使用未来,80年代写了一个框架,90人写的代码。 多态是软件行业追寻的一个目标。
6.1.6 多态成立的条件
1) 要有继承;
2) 要有虚函数重写;
3) 要有父类指针(父类引用)指向子类对象。
6.1.7 静态联编和动态联编
1)联编是指一个程序模块、代码之间互相关联的过程。
2)静态联编(staic binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
3)动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 语句和 if 语句是动态联编的例子。
6.2 虚构函数
1)构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
2)析构函数可以是虚的。虚析构函数用于指引delete运算符正确析构动态对象。
#include <iostream>
using namespace std;
class A
{
private:
char *p;
public:
void A(void)
{
p = new char[20];
strcpy(p,"obja");
}
virtual ~A()
{
delete [] p;
printf("~A()\n");
}
};
class B:public A
{
private:
char *p;
public:
void B(void)
{
p = new char[20];
strcpy(p,"objb");
printf("B()\n");
}
~B()
{
delete [] p;
printf("~B()\n");
}
};
class C:public B
{
private:
char *p;
C()
{
p = new char[20];
strcpy(p,"objc");
printf("C()\n");
};
~C()
{
delete [] p;
printf("~C()\n");
};
};
//通过⽗类指针 把 所有的⼦类对象的析构函数 都执⾏⼀遍
//通过⽗类指针 释放所有的⼦类资源
void howtodelete(A *base)
{
delete base;
}
int main(int argc,char **argv)
{
C *myC = new C;
howtodelete(myC);//通过⽗类的指针调⽤释放⼦类的资源
return 0;
}
6.3 重载、重写、重定义
重载(添加):
a.相同范围(在同一个类中);
b.函数名字相同;
c.参数不同;
d.virtual关键字可有可无。
重写(覆盖):是指派生类函数覆盖基类函数,特征是:
a.不同的范围,分别位于基类和派生类中;
b.函数名字相同;
c.参数相同;
d.基类函数中必须有virtual关键字。
重定义(隐藏) 是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a.如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏;
b.如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数被隐藏。
6.4 多态实现原理
6.4.1 虚函数表和vptr指针
a.当类中声明虚函数时,编译器会在类中生成一个虚函数表;
b.虚函数表是一个存储类成员函数指针的数据结构;
c.虚函数表是由编译器自动生成与维护的;
d.virtual成员函数会被编译器放入虚函数表中;
e.存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
说明:
a)通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
b)出于效率考虑,没有必要将所有成员函数都声明为虚函数。
c) C++编译器,执行run函数,不需要区分是子类对象还是父类对象,而是直接通过p的VPTR指针所指向的对象函数执行即可。
6.4.2 证明vptr指针的存在
#include <iostream>
using namespace std;
class Parent1
{
public:
int a;
private:
Parent1(int a=0)
{
this->a = a;
}
void print()
{
cout <<"我是爹"<<endl;
}
};
class Parent2
{
public:
int a;
private:
Parent2(int a=0)
{
this->a = a;
}
void print()
{
cout <<"我也是爹"<<endl;
}
};
int main()
{
cout <<"sizeof(Parent1): " << sizeof(Parent1) <<endl;
cout <<"sizeof(Parent2): " << sizeof(Parent2) <<endl;
return 0;
}
6.4.3 构造函数中能否调用虚函数,实现多态?
a.对象在创建的时,由编译器对VPTR指针进行初始化;
b.只有当对象的构造完全结束后VPTR的指向才最终确定;
c.父类对象的VPTR指向父类虚函数表;
d.子类对象的VPTR指向子类虚函数表。
#include <iostream>
using namespace std;
//构造函数中调⽤虚函数能发⽣多态吗?
class Parent
{
private:
int a;
Parent(int a=0)
{
this->a = a;
print();
}
virtual void print()
{
cout<<"我是爹"<<endl;
}
};
class Child:public Parent
{
private:
int b;
public:
Child(int a = 0, int b=0):Parent(a)
{
this->b = b;
print();
}
virtual void print()
{
cout<<"我是⼉⼦"<<endl;
}
};
void HowToPlay(Parent *base)
{
base->print(); //有多态发⽣
}
int main(void)
{
//定义⼀个⼦类对象,在这个过程中,在⽗类构造函数中调⽤虚函数print能发⽣多态吗?
Child c1;
HowToPlay(&c1);
return 0;
}
6.4.4 父类指针和子类指针的步长
#include <iostream>
using namespace std;
class Parent
{
private:
int a;
public:
Parent(int a=0)
{
this->a = a;
}
virtual void print()
{
cout<<"我是爹"<<endl;
}
};
class Child:public Parent
{
private:
int a;
public:
Child(int b = 0):Parent(0)
{
this->b = b;
}
virtual void print()
{
cout<<"我是⼉⼦"<<endl;
}
};
int main()
{
Parent *pP = NULL;
Child *pC = NULL;
Child array[] = {Child(1), Child(2), Child(3)};
pP = array;
pC = array;
pP->print();//发⽣多态
pC->print();
pP++;
pC++;
pP->print();//发⽣多态
pC->print();
return 0;
}
6.5 有关多态的理解
多态的实现效果:
多态:同样的调用语句有多种不同的表现形态;
多态实现的三个条件:
有继承、有virtual重写、有父类指针(引用)指向子类对象;
多态的C++实现:
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。
多态的理论基础:
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。
多态的重要意义:
设计模式的基础 是框架的基石。
多态原理探究:
虚函数表和vptr指针。
6.6 纯虚函数和抽象类
6.6.1 基本概念
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本。
纯虚函数为派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。
纯虚函数的语法:
virtual 类型 函数名(参数表) = 0;
一个具有纯虚函数的基类称为抽象类。
6.6.2 纯虚函数和抽象类
#include <iostream>
using namespace std;
⾯向抽象类编程(⾯向⼀套预先定义好的接⼝编程)
class Figure //抽象类
{
public:
//提供⼀个统⼀的界⾯(接⼝),让⼦类使⽤,让⼦类必须去实现
virtual void getArea() = 0 ; //纯虚函数
};
class Circle:public Figure
{
private:
int r;
public:
Tri(int r)
{
this->r = r;
}
virtual void getArea()
{
cout<<"圆的面积的⾯积: "<<3.14*a*a<<endl;
}
};
class Tri:public Figure
{
public:
Tri(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea()
{
cout<<"三⾓形的⾯积: "<<a*b/2<<endl;
}
private:
int a;
int b;
};
class Square:public Figure
{
private:
int a;
int b;
public:
Square(int a,int b)
{
this -> a = a;
this -> b = b;
}
virtual void getArea()
{
cout<<"四边形的面积: "<<a*b<<endl;
}
};
void area_func(Figure *base)
{
base->getArea(); //会发⽣多态
}
int main()
{
//Figure f; //抽象类不能被实例化
Figure *base = NULL; //抽象类不能被实例化
Circle c1(10);
Tri t1(20, 30);
Square s1(50, 60);
//⾯向抽象类编程(⾯向⼀套预先定义好的接⼝编程)
area_func(&c1);
area_func(&t1);
area_func(&s1);
return 0;
}
1、含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在 的意义就是被继承,提供族类的公共接口。
2、纯虚函数只有声明,没有实现,被“初始化”为 0。
3、如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数,派生类仍然为纯虚基类。
6.6.3 抽象类在多继承中的应用
绝大多数面向对象语言都不支持多继承,绝大多数面向对象语言都支持接口的概念。
C++中没有接口的概念,C++中可以使用纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据的定义。
6.7 面向抽象类编程案例
#include <iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout<<"Animal::Animal()"<<endl;
}
~Animal()
{
cout<<"Animal::~Animal()"<<endl;
}
virtual void voice() = 0;
};
class Dog:public Animal
{
public:
Dog()
{
cout<<"Dog::Dog()"<<endl;
}
~Dog()
{
cout<<"Dog::~Dog()"<<endl;
}
virtual void voice()
{
cout<<"wang wang"<<endl;
}
};
class Cat:public Animal
{
public:
Cat()
{
cout<<"Cat::Cat()"<<endl;
}
~Cat()
{
cout<<"Cat::~Cat()"<<endl;
}
virtual void voice()
{
cout<<"miao miao "<<endl;
}
};
int main(void)
{
// Animal ani; 抽象基类,不能实例化。
Animal *pa = new Dog;
pa->voice();
delete pa;
pa = new Cat;
pa->voice();
delete pa;
return 0;
}
6.8 C语言中的面向接口编程
函数三要素: 名称、参数、返回值
6.8.1 函数类型语法基础
函数指针用于指向一个函数, 函数名是函数体的入口地址;
1)可通过函数类型定义函数指针: FuncType* pointer;
2)也可以直接定义:type (*Funpointer)(parameter list); // Funpointer为函数指针名
//type为指向函数的返回值类型
//parameter list为指向函数的参数类型列表
#include <stdio.h>
typedef int(FUNC)(int);//定义⼀个函数类型
int test(int i) //定义⼀个函数
{
return i*i;
}
void f() //定义⼀个函数
{
printf("Call f()...\n");
}
int main()
{
FUNC* pt = test; //定义⼀个指向函数的类型FUNC的指针pt,
//并初始化指向 test 函数
printf("Function pointer call: %d\n", pt(3));
//通过函数指针pt 间接调⽤test
void(*pf)() = &f; //直接定义⼀个函数指针 指向f
pf(); //通过函数指针间接调⽤
(*pf)(); //通过函数指针间接调⽤ 等价于上述调⽤⽅法
return 0;
}
6.8.1 函数指针做函数参数
当函数指针 做为函数的参数,传递给一个被调用函数,被调用函数就可以通过这个指针调用外部的函数,这就形成了回调。
#include <stdio.h>
int add(int a,int b);
int libfun(int (*pDis)(int a,int b));
int main(void)
{
int (*pfun)(int a,int b);
//定义⼀个函数指针pfun指向int ()(int, int)函数类型
pfun = add;
libfun(pfun);
return 0;
}
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函数
return 0;
}
回调函数的优点
1)函数的调用 和 函数的实现 有效的分离;
2)类似C++的多态,可扩展
现在这几个函数是在同一个文件当中
int libfun(int (*pDis)(int a, int b))
是一个库中的函数,就只有使用回调了,通过函数指针参数将外部函数地址传入来实现调用。
函数 add 的代码作了修改,也不必改动库的代码,就可以正常实现调用 便于程序的维护和升级。