C++学习大纲
**知识无底,学海无涯,我将我学习c++学到的知识,学习这部分前,建议有一部分c
语言的基础,这样学起来更容易掌握如下向大家分享,学识低浅,如有错误,
请君提出,方便我修改。**
一、前言
- C++是C语言的继承,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计,因而C++就适应的问题规模而论,大小由之。.C++不仅拥有计算机高效运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。
发展历程 - 世界上第一种计算机高级语言是诞生于1954年的FORTRAN语言。之后出现了多种计算机高级语言。1970年,AT&T的Bell实验室的D.Ritchie和K.Thompson共同发明了C语言。C语言充分结合了汇编语言和高级语言的优点,高效而灵活,又容易移植。
1971年,瑞士联邦技术学院N.Wirth教授发明了Pascal语言。Pascal语言语法严谨,层次分明,程序易写,具有很强的可读性,是第一个结构化的编程语言。 20世纪70年代中期,Bjarne Stroustrup在剑桥大学计算机中心工作。他使用过Simula和ALGOL,接触过C。他对Simula的类体系感受颇深,对ALGOL的结构也很有研究,深知运行效率的意义。既要编程简单、正确可靠,又要运行高效、可移植,是Bjarne Stroustrup的初衷。 1979年,Bjame Sgoustrup到了Bell实验室,开始从事将C改良为带类的C(C with classes)的工作。1983年该语言被正式命名为C++。 Alexander stepanov创建了标准模板库(Standard Template Library,STL)。STL不仅功能强大,同时非常优雅,然而,它也是非常庞大的。
C++语言的使用领域:
- 游戏开发:强建模能力,性能高。
- 科学计算:FORTRAN,C++算法库。
- 网络和分布式:ACE框架。
- 桌面应用:VC/MFC,Office,QQ,多媒体
- 操作系统和设备驱动:优化编译器的发明使C++在底层开发方面可
以和C向媲美。 - 移动终端
既需要性能,同时又要有面向对象的建模。
语言特点
- 支持数据封装和数据隐藏:在C++中,类是支持数据封装的工具,对象则是数据封装的实现。C++通过建立用户定义类支持数据封装和数据隐藏。在面向对象的程序设计中,将数据和对该数据进行合法操作的函数封装在一起作为一个类的定义。对象被说明为具有一个给定类的变量。每个给定类的对象包含这个类所规定的若干私有成员、公有成员及保护成员。完好定义的类一旦建立,就可看成完全封装的实体,可以作为一个整体单元使用。类的实际内部工作隐藏起来,使用完好定义的类的用户不需要知道类是如何工作的,只要知道如何使用它即可。
- 支持继承和重用:在C++现有类的基础上可以声明新类型,这就是继承和重用的思想。通过继承和重用可以更有效地组织程序结构,明确类间关系,并且充分利用已有的类来完成更复杂、深入的开发。新定义的类为子类,成为派生类。它可以从父类那里继承所有非私有的属性和方法,作为自己的成员。
- 支持多态性:采用多态性为每个类指定表现行为。多态性形成由父类和它们的子类组成的一个树型结构。在这个树中的每个子类可以接收一个或多个具有相同名字的消息。当一个消息被这个树中一个类的一个对象接收时,这个对象动态地决定给予子类对象的消息的某种用法。多态性的这一特性允许使用高级抽象。

学习c++之前,首先要了解, 什么是对象?
- 万物皆对象
- 程序就是一组对象,对象之间通过消息交换信息
- 类就是对对象的描述和抽象,对象就是类的具体化和实例化
通过类描述对象
- 属性:姓名、年龄、学号
- 行为:吃饭、睡觉、学习
- 类就是从属性和行为两个方面对对象进行抽象。
面向对象程序设计(OOP) 现实世界 虚拟世界 对象 -> 抽象 -> 类 -> 对象 1.至少掌握一种OOP编程语言 2.精通一种面向对象的元语言—UML 3.研究设计模式,GOF 有了这一部分的知识,让我们开始踏上c++的学习之旅吧! 温馨提示
一、C++ 基本数据类型和表达式
1.C++是一种静态类型语言(运行前指定每个数据的类型),也是一种强类型语言(对数据的操作进行严格的类型检查)。
- bool类型数据在算术运算时true对应1,false对应0。
【注意】关系运算符结合型均为左结合右,整体优先级低于算术运算符,高于赋值运算符 在C++中用数值1代表“真”,用0代表“假”
-
typedef给已有类型取别名 typedef <已有类型> <别名>; -
表达式中的类型转换 static_cast <类型说明符> (表达式) 用于一般表达式的类型转换 reinterpret_cast <类型说明符> (表达式) 用于非标准的指针数据类型转换 const_cast <类型说明符>( 表达式) 将const表达式转换成非常量类型,常用于将限制const成员函数const定义解除 dynamic_cast <类型说明符>(表达式) 用于进行对象指针的类型转换 -
表达式中的类型转换 static_cast <类型说明符> (表达式) 用于一般表达式的类型转换 reinterpret_cast <类型说明符> (表达式) 用于非标准的指针数据类型转换 const_cast <类型说明符>( 表达式) 将const表达式转换成非常量类型,常用于将限制const成员函数const定义解除 dynamic_cast <类型说明符>(表达式) 用于进行对象指针的类型转换 -
常量包括两种:字面常量和符号常量。 字面常量:直接写出来的 符号常量:又称命名常量,指有名字的常量,如 const double PI=3.1415; #define PI 3.1415 在定义变量时,如果在前面加上关键字const,则变量的值在程序运行期间不能被改变, 格式 const 类型说明符 变量名 注意: 在定义常变量时必须同时对变量进行初始化,此后它的值不能再改变,即不能给它赋值 在C标准中,const 定义的常量是全局的,但C++中视声明位置而定 #define 和const的区别: const常量有数据类型,而宏变量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只能进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误, 有些集成化的调试工具可以对const常量进行调试,但不能对宏变量进行调试。 -
符号常量 用一个符号名来代替一个常量,称为符号常量。 使用预处理命令#define 指定的 #define 格式(宏定义命令) 简单的宏定义 #define <宏名> <字符串> 带参数的宏定义 #define <宏名> (<参数表>) <宏体> 符号常量是个名字,但它不是变量,在它的作用域内其值不能改变,也不能赋值。 符号常量的作用 1)增加程序易读性 2)提高程序对常量使用的一致性 3)增强了程序的易维护性 -
自增自减运算符 前缀运算是先变化后运算,后缀运算是先运算后变化。 -
变量定义要给变量分配内存空间,而声明没有;定义可以初始化,声明不能。 声明: extern <类型名> <变量名>; 头文件中使用extern语句对一个文件中的全局函数进行声明;
函数使用一个全局变量但是还未见到其定义时使用extern语
句对其进行声明。
-
逻辑运算符与表达式 优先级(高到低) !(逻辑非) &&(逻辑与) ||(逻辑或) 对于逻辑与运算,如果第一个操作数被判断为假, 系统不再判断或求解第二操作数 对于逻辑非运算,如果第一个操作数被判断为真,系统不再判断或求解第二操作数 -
当运算结果已经确定时,后面的表达式就不会再执行。 -
类型转换 隐式类型转换 -> 显示类型转换 int i=-10; unsigned int j=1; i < j 的值是false,而不是true -
操作符的优先级 1)按单目、双目、三目、赋值依次降低 2)按算术、移位、关系、逻辑位、逻辑依次降低 -
表达式中操作数的类型转换 逐个操作符进行类型转换
如: short int a; int b; double c;
a*b/c; 先a > int, 然后(a*b) > double
-
位运算符与表达式 & | ^ ~ << >> 按位与(&) 按位与运算的作用是将两个操作数对应的每一位分别进行逻辑与运算 按位或(|) 按位或运算的作用是将两个操作数对应的每一位分别进行逻辑或运算 按位异或(^) 按位异或操作的作用是将两个操作数对应的每一位进行异或。 运算规则 若对应位相同,则该位的运算结果为0;若对应位不同,则该位的运算结果为1 按位取反(~) 按位取反是一个单目运算符,其作用是对一个二进制数的每一位取反 左移位(<<) 按照指定的位数将一个数的二进制值向左移位,左移后,低位(右边空位)补0,移出的高位(左边)舍弃 右移位(>>) 按照指定的位数将一个数的二进制值向右移位,右移后移出的低位舍弃,如果是无符号数则高位补零,如果是有符号数,则高位补符号位 -
SIZEOF运算符 返回指定的数据类型或表达式值的数据类型在内存中占用的字节数 两种形式 sizeof(类型说明符) sizeof(char) sizeof(表达式) sizeof(66) -
逗号运算符 逗号运算符的优先级别最低,结合方向自左至右,其功能是把两个表达式连接起来组成一个表达式,称为逗号表达式 格式: 表达式1,表达式2,表达式3,……,表达式n 注意 计算一个逗号表达式的值时,从左至右依次计算各个表达式的值,最后计算的一个表达式的值和数据类型便是整个逗号表达式的值和类型 x=25,x4; 逗号表达式可以嵌套 -
敲重点: 计算过程中要注意数据的底层表示(是否溢出等)、表达式的副作用(短路求值等)。
二、C++ 子程序间的数据传递
-
通过全局变量 全局变量可以定义在函数外的任何地方,但是如果在使用一个全局变量时未见到它的定义,就要使用extern语句对其进行声明。 -
通过子程序的参数和返回值机制 1)值传递:传递实参的一个拷贝,可以阻止子程序通过形参改变实参,但最多只能返回一个值 2)地址/引用传递:传递实参的地址,可以提高参数传递的效率,可以返回多个执行结果,但是会降低数据访问效率(通过间接的方式访问传输的数据)、可通过形参改变实参
三、C++ 函数的返回值
- 返回值:返回任意类型的数据类型,会将返回数据做一个拷贝(副本)赋值给变量;由于需要拷贝,所以对于复杂对象这种方式效率比较低(调用对象的拷贝构造函数、析构函数);例如:int test(){}或者 Point test(){}
- 返回指针:返回一个指针,也叫指针类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int *test(){} 或者 Point *test(){}
返回引用:返回一个引用,也叫引用类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构 - 函数和析构函数;例如:int &test(){}或者 Point &test(){}
一般来说,在函数内对于存在栈上的局部变量的作用域只在函数内部,在函数返回后,局部变量的内存会自动释放。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错;但是如果返回的是局部变量的地址(指针)的话,就会造成野指针,程序运行会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放,这样指针指向的内容就是不可预料,调用就会出错。
1、返回值
int test1()
{
int a = 1;
retur a;
}
返回值是最简单有效的方式,他的操作主要在栈上,根据函数栈的特性局部变量a会在函数结束时被删除,为了返回a的值,需要产生a的复制。如果a原子类型这当然也无所谓,但是如果a是大的对像,那么对a的复制将会产生比较严重的资源和性能消耗。注意函数返回值本身因为没有名称或引用是右值,是不能直接操作的。
2、返回指针
int* test2()
{
int *b = new int();
*b = 2;
return b;
}
返回指针是在C中除了返回值以外的唯一方式,根据函数栈的特性也会产生复制,但是这个复制只是4(8)字节的指针,对于返回大型对像来说确实能够减少不少的资源消耗。但是返回指针资源的清理工作交给了调用者,这某种意义上违反了谁申请谁销毁的原则。指针也是右值同样无法操作。
3、返回引用
int& test2()
{
int *b = new int();
*b = 2;
return *b;
}
引用是C++中新添加的概念,所以返回引用也是C++中相对于C来说所没有的。引用是值的别名,和指针一样不存在对大对像本身的复制,只是引用别名的复制。引用是左值,返回引用可以直接操作,也就可以进行连续赋值,最经典的示例是拷贝构造函数和运算符重载一般返回引用。 test2()+=1; 但是,返回引用会带来一个问题,那就是返回局部变量内存空间,会产生异常,也就是说局部变量是不能作为引用返回的。局部指针可以作为引用返回但是和返回指针一样需要调用者自己去清理内存。
而main函数通过返回值把整个程序的执行情况告诉调用者(通常是操作系统,但是操作系统通常会忽视main函数
的返回值),一般情况下return 0表示正常结束,return -1表示非正常结束。
main函数也可以不写return语句,这时当执行完最后一条语句后自动执行一条“return 0;”语句。
四、C++ 标识符的作用域和名字空间
包括:局部作用域、全局作用域、文件作用域、函数作用域、名空间作用域、类作用域
注意:潜在作用域,也就是同名变量的作用域问题。 识符的作用域:
-
局部作用域 指在函数定义或者复合语句中,从标识符的定义点开始到函数或者复合语句结束之间的程序段。 在同一个局部作用域内不能出现相同名字的两个局部变量(包括形参)。 一个函数内的复合语句又是一个局部作用域,也就是在函数内有某个变量时,复合语句中可以有另外一个同名字的变量。 -
全局作用域 指对构成C++程序的所有源文件。 在C++标准中,把全局作用域归于连接控制范畴。 通常把全局标识符的生命放在某个头文件中。
标识符的作用域是其最进的一对大括号。
#include <iostream>
using namespace std;
int i;
namespace Ns {
int j;
}
int main() {
i = 5;
Ns::j = 6;
{
using namespace Ns;
int i;
i = 7;
cout << "i = " << i << endl;
cout << "j = " << j << endl;
}
cout << "i = " << i << endl;
return 0;
}
- 对象的生存期:
static声明的局部变量生存期是整个程序 static语句只有最开始使用一次,a,b的值存储在内存中,第二次调用other使用上次other结束时的a,b值(a=4,b=4)。
#include <iostream>
using namespace std;
int i = 1;
void other() {
static int a = 2;
static int b;
int c = 10;
a += 2;
i += 32;
c += 5;
cout << "---OTHER---" << endl;
cout << " i: " << i << " a: " << a << " b: " << b << " c: " << c << endl;
b = a;
}
int main() {
static int a;
int b = -10;
int c = 0;
cout << "---MAIN---" << endl;
cout << " i: " << i << " a: " << a << " b: " << b << " c: " << c << endl;
c += 8;
other();
cout << "---MAIN---" << endl;
cout << " i: " << i << " a: " << a << " b: " << b << " c: " << c << endl;
i += 10;
other();
return 0;
}
-
文件作用域 指单独的一个源文件。 在全局标识符的定义中加上static修饰符,该全局标识符就变成了具有文件作用域的标识符。 量在本文件中定义。
量在本文件中定义直接访问。。
int counter;
cout << counter++ << endl;
变量在其它文件中定义。
在访问前先要声明,然后才可以使用
int counter;
extern int counter;
cout << couter++ << endl;
#include file_1.cpp
- 名空间作用域
#include "Fasdsad.h"
#include <iostream>
Fasdsad::Fasdsad()
{
}
Fasdsad::~Fasdsad()
{
}
using namespace std;
int i;
int k = 100;
namespace Mode {
int j;
}
int main() {
i = 5;
Mode::j = 4;
{
using namespace Mode;
int i = 2;
j = 10;
int f = 20;
cout << "i=" << i << endl;
cout << "j=" << j << endl;
cout << "k=" << k << endl;
}
cout << "i=" << i << endl;
system("pause");
return 0;
}
-
名字空间 -
对程序中的标识符(类型、函数、变量), 按照某种逻辑规则划分成若干组。 -
定义名字空间 namespace 名字空间名 { 名字空间成员; } -
使用名字空间 1.作用于限定符:名字空间名::名字空间成员, 表示访问特定名字空间中的特定成员。 例子:
#include <iostream>
int main (void) {
std::cout << "Hello, World !" << std::endl;
int i;
double d;
char s[256];
std::cin >> i >> d >> s;
std::cout << i << ' ' << d << ' ' << s << '\n';
return 0;
}
- 名字空间指令:
using namespace 名字空间名; 在该条指令之后的代码对指令所指名字空间中的所有成员都可见, 可直接访问这些成员,无需加“::”。 例:using namespace std; - 名字空间声明:
using 名字空间名::名字空间成员; 将指定名字空间中的某个成员引入当前作用域, 可直接访问这些成员,无需加“::”。 - 匿名名字空间
如果一个标识符没有被显示地定义在任何名字空间中, 编译器会将其缺省地置于匿名名字空间中。 对匿名名字空间中的成员通过“::名字空间成员”的形式访问。 - 名字空间合并
- 名字空间嵌套
namespace ns1 {
namespace ns2 {
namespace ns3 {
void foo (void) { ... }
}
}
}
ns1::ns2::ns3::foo ();
using namespace ns1::ns2::ns3;
foo ();
例子:名字空间
#include <iostream>
using namespace std;
void print (int money) {
cout << money << endl;
}
namespace abc {
int balance = 0;
void save (int money) {
balance += money;
}
void draw (int money) {
balance -= money;
}
}
namespace abc {
void salary (int money) {
balance += money;
}
void print (int money) {
cout << "农行:";
::print (money);
}
}
namespace ccb {
int balance = 0;
void save (int money) {
balance += money;
}
void draw (int money) {
balance -= money;
}
void salary (int money) {
balance += money;
}
}
int main (void) {
using namespace abc;
save (5000);
cout << "农行:" << balance << endl;
draw (3000);
cout << "农行:" << balance << endl;
ccb::save (8000);
cout << "建行:" << ccb::balance << endl;
ccb::draw (5000);
cout << "建行:" << ccb::balance << endl;
using ccb::salary;
salary (6000);
cout << "建行:" << ccb::balance << endl;
abc::print (abc::balance);
return 0;
}
五、C++ 宏与内联函数
因为函数调用需要开销(如:保护调用者的运行环境、参数传递、执行调用指令等),所以函数调用会带来程序执行效率的下降,特别是对一些小函数的频繁调用将是程序的效率有很大的降低。
C++提出了两种解决方法:宏、内联函数。
- 宏
宏定义是一种简单的语义替换。 它在元编程时有两种风格
#define identifier replacement-list
#define identifier(a1,a2,...an) replacement-list
宏定义的4种格式:
1)#define <宏名> <文字串>
在编译前进行使用文字串进行宏替换
#define PI 3.14
2)#define <宏名>(<参数表>) <文字串>
在编译前进行使用文字串进行宏替换
#define max(a,b) a>b?a:b
3)#define <宏名>
只是告诉编译程序该宏名已经被定义,不做任何文字串替换,其用于条件编译
如:#define OUTPUTFILE
#ifdef OUTPUTFILE
//输出到文件的代码
#endif
4)#undef <宏名>
用于取消宏名的编译
不足:1)重复计算,如max((x+1),(y+2)),因为其只是进行单纯的文字替换
2)不进行参数类型检查和转换
3)不利于一些工具对程序的处理(如C++程序编译后,所有宏都不存在了)
C++之父的建议:少用宏,多用const、enum和inline
- 内联函数
内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。 常规函数调用使程序跳到另一个地址(函数的地址),并在函数结束时返回。
C++内联函数的编译代码与其他程序代码“内联”起来,也就是说,编译器将使用相应的函数代码代替函数调用,因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
要使用这项特性,必须采取下述措施之一:
在函数声明前加上关键字inline; 在函数定义前加上关键字inline。
其作用是建议(具体有没有不一定,有些函数加上也不会作为内联函数对待,如递归函数)编译程序把该函数的函数体展开到调用点,这样就避免了函数调用的开销,从而提高了函数调用的效率。
inline int max(int a,int b)
{return a>b?a:b;}
注意:内联函数名具有文件作用域。
- 这两者的区别
内联函数的优势在于做参数类型检查,而宏定义不会,宏只是简单的文本替换。 见如下实例——
#define SQUARE(X) X*X
a = SQUARE(5.0);
b = SQUARE(4.3 + 3.1);
c = SQUARE(d++)
宏定义时不能忘记括号的使用,否则会造成运算错误。但是上述代码中,加上括号仍然会出现错误,在第三个函数中,d依旧递增两次。如果使用内联函数,则是计算出 d 的值在进行传递,并进行运算,就只会递增一次。
六、C++ 函数重载
函数重载:在C语言中,一个函数不能与另一个函数重名,而在C++中,只要一个函数的参数列表与另一个函数的参数列表不完全相同,函数名就可以相同。 C++这一特点就是所谓函数的重载现象。 同一个名字因为参数列表不同,展现了不同的结果,也叫静多态。
- 重载原则:
①函数名相同,函数参数列表不同(类型、个数、顺序) ②匹配原则1:严格匹配,找到再调用 ③匹配原则2:通过隐式类型转换寻求一个匹配,找到则调用 ④返回值类型不构成重载条件
简单来说,就是 在同一个作用域中, 函数名相同, 参数表不同的函数, 构成重载关系。
关于操作符重载的限制 1.至少有一个操作数是类类型的。 int a = 10, b = 20; int c = a + b; // 200 int operator+ (int a, int b) { return a * b; } // ERROR ! 2.不是所有的操作符都能重载。 :: - 作用域限定 . - 直接成员访问 .* - 直接成员指针解引用 ?: - 三目运算符 sizeof - 获取字节数 typeid - 获取类型信息 3.不是所有的操作符都可以用全局函数的方式实现。 = - 拷贝赋值 [] - 下标 () - 函数 -> - 间接成员访问 4.不能发明新的操作符,不能改变操作数的个数 x ** y; // x^y
#include <iostream>
using namespace std;
class Complex {
public:
Complex (int r = 0, int i = 0) :
m_r (r), m_i (i) {}
void print (void) const {
cout << m_r << '+' << m_i << 'i' << endl;
}
const Complex operator+ (const Complex& r) const {
return Complex (m_r + r.m_r, m_i + r.m_i);
}
private:
int m_r;
int m_i;
friend const Complex operator- (const Complex&,
const Complex&);
};
const Complex operator- (const Complex& l,const Complex& r) {
return Complex (l.m_r - r.m_r, l.m_i - r.m_i);
}
int main (void) {
const Complex c1 (1, 2);
c1.print ();
const Complex c2 (3, 4);
Complex c3 = c1 + c2;
c3.print ();
c3 = c2 - c1;
c3.print ();
return 0;
}
#include <iostream>
using namespace std;
class Complex {
public:
Complex (int r = 0, int i = 0) :
m_r (r), m_i (i) {}
void print (void) const {
cout << m_r << '+' << m_i << 'i' << endl;
}
Complex& operator+= (const Complex& r) {
m_r += r.m_r;
m_i += r.m_i;
return *this;
}
friend Complex& operator-= (Complex& l,
const Complex& r) {
l.m_r -= r.m_r;
l.m_i -= r.m_i;
return l;
}
private:
int m_r;
int m_i;
};
int main (void) {
Complex c1 (1, 2), c2 (3, 4);
c1 += c2;
c1.print ();
Complex c3 (5, 6);
(c1 += c2) = c3;
c1.print ();
c1 -= c2;
c1.print ();
(c1 -= c2) = c3;
c1.print ();
return 0;
}
#include <iostream>
using namespace std;
class Complex {
public:
Complex (int r = 0, int i = 0) :
m_r (r), m_i (i) {}
void print (void) const {
cout << m_r << '+' << m_i << 'i' << endl;
}
friend ostream& operator<< (ostream& os,
const Complex& r) {
return os << r.m_r << '+' << r.m_i << 'i';
}
friend istream& operator>> (istream& is,
Complex& r) {
return is >> r.m_r >> r.m_i;
}
private:
int m_r;
int m_i;
};
int main (void) {
Complex c1 (1, 2), c2 (3, 4);
cout << c1 << endl << c2 << endl;
cin >> c1 >> c2;
cout << c1 << endl << c2 << endl;
return 0;
}
缺省参数和哑元参数
- 如果调用一个函数时,没有提供实参,那么对应形参就取缺省值。
- 如果一个参数带有缺省值,那么它后边的所有参数必须都带有缺省值。
- 如果一个函数声明和定义分开,那么缺省参数只能放在声明中。
- 避免和重载发生歧义。
- 只有类型而没有名字的形参,谓之哑元。
i++ - operator++ ++i V1: void decode (int arg) { … } V2: void decode (int) { … }
例子1:重载与缺省值
#include <iostream>
using namespace std;
void foo (int a = 10, double b = 0.01,
const char* c = "tarena");
void foo (void) {}
void bar (int) {
cout << "bar(int)" << endl;
}
void bar (int, double) {
cout << "bar(int,double)" << endl;
}
int main (void) {
foo (1, 3.14, "hello");
foo (1, 3.14);
foo (1);
bar (100);
bar (100, 12.34);
return 0;
}
例子2:重载与作用域
#include <iostream>
using namespace std;
namespace ns1 {
int foo (int a) { 函数1
cout << "ns1::foo(int)" << endl;
return a;
}
};
namespace ns2 {
double foo (double a) { 函数2
cout << "ns2::foo(double)" << endl;
return a;
}
};
int main (void) {
using namespace ns1;
using namespace ns2;
cout << foo (10) << endl;
cout << foo (1.23) << endl;
using ns1::foo;
(当同时出现名字指令与名字空间声明,则名字空间声明会隐藏名字空间指令)
cout << foo (10) << endl;
cout << foo (1.23) << endl;
using ns2::foo;
cout << foo (10) << endl;
cout << foo (1.23) << endl;
return 0;
}
还有更多的例子,这里就不再展示,大家自行上网搜索即可。
七、C++ 条件编译
使用编译预处理命令对编译过程进行知道,决定哪些代码需要编译。
指令 用途
# 空指令,无任何效果
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码,其实就是else if的简写
#endif 结束一个#if……#else条件编译块
#error 停止编译并显示错误信息
1. 格式1
#ifdef <宏名> / #ifndef <宏名>
<程序段1>
#else
<程序段2>
#endif
2. 格式2
#ifdef <常量表达式1> / ifdef <宏名> / #ifndef <宏名>
<程序段1>
#elif <常量表达式2>
<程序段2>
#elif <常数表达式3>
<程序段3>
#else
<程序段4>
#endif
例子:
题目:输入一个字母字符,使之条件编译,使之能根据需要将小写字母转化为大写字母输出,或将大写字母转化为小写字母输出。
#include<iostream>
using namespace std;
#define upper 1
int main(){
char a;
#if upper
cout<<"lowercase to uppercase"<<endl;
cout<<"please input a char:";
cin>>a;
if(a>='a'&&a<='z'){
cout<<a<<"===>"<<char(a-32)<<endl;
}else{
cout<<"data erroe"<<endl;
}
#else
cout<<"uppercase to lowercase"<<endl;
cout<<"please input a char:";
cin>>a;
if(a>='A'&&a<='Z'){
cout<<a<<"===>"<<char(a+32)<<endl;
}else{
cout<<"data erroe"<<endl;
}
#endif
cout<<"Good Night~"<<endl;
return 0;
}
由于LETTER为真,对第一个if语句进行编译,将小写字母转换成大写字母,输出'LANGUAGE';若LETTER为假,编译第二个语句块,输出为小写;
八、C++ 枚举类型
在了解枚举类型之前,我们要先知道我们为什么用枚举? 先看看枚举怎么个用途,其实枚举是很实用的一个工具,主要体现在代码阅读方面。
设想这样一个场景,一个项目,写了上千行,有些常量类型,只有几个固定的取值,在写的时候为了图方便,可以就用数字表示(如0,1,2,3),比如颜色,状态等。这样固然方便,且节省内存,但过了一个月再想看明白这个代码,就不容易了吧。
再退一步,拿颜色举例,有时要用上七八种颜色,如果用数字表示,对应起来也是极不方便,还得想半天,这时,如果颜色就用名字表示,但在内存中还是数字,就舒服得多了。
枚举类型是一种可以由用户自定义数据集的数据类型。
注意:bool类型可以看成是C++语言提供的一个预定义的枚举类型。
-
枚举类型定义 enum <枚举类型名> {<枚举值表>}; -
初始化 枚举类型的每一个枚举值都对应一个整型数,默认情况下,第一个枚举值的值是0,然后依次增1,但也可以显示初始化任意一个枚举值对应的整形数,没定义的枚举值默认情况下在其前一个枚举值的对应整型数上加1. 留个问题:如果多个枚举值对应同一个整形数会怎样? enum Day {Sun=7, MON=1, TUE, WED, THU, FRI, SAT} -
枚举变量的定义 <枚举类型> <变量表>;
或<枚举类型>{<枚举值表>} <变量表>;
-
枚举变量的使用 1)赋值 Day d1,d2; d1 = SUN; //true
d2 = 3; //error, 但int n = SUN;也是可以的
d2 = (Day)3;//true 但这样不安全,必须要保证该整型数属于枚举类型的值集,否则没有意义
2)比较运算 MON < TUE的结果为true,运算时将其转换为整型
3)算术运算 d2 = d1 + 1;//error,因为它d1 + 1的结果是整型
d2 = (Day)(d1 + 1);//true
4)其他 输入输出:可以输入int数,使用switch,然后复制或者输出
类下标访问:day(0)对应的是第一个枚举值sun
- 重要提示
枚举变量可以直接输出,但不能直接输入。如:cout >> color3; //非法 不能直接将常量赋给枚举变量。如: color1=1; //非法 不同类型的枚举变量之间不能相互赋值。如: color1=color3; //非法 枚举变量的输入输出一般都采用switch语句将其转换为字符或字符串;枚举类型数据的其他处理也往往应用switch语句,以保证程序的合法性和可读性。
九、C++ 数组类型
数组类型是一种有固定多个同类型的元素按一定次序所构成的数据类型。
数组名字的命名规则跟变量是一样的,只能使用数字、字母、下划线,而且数字不能做开头。
访问数组 当我们需要访问数组的时候,也就是需要根据下标来访问。 例如 int nums[60];它的下标范围是0-59,从0开始,注意下标访问的数字,防止访问越界地出现。
数组是数目固定,类型相同的若干变量的有序集合。
数组的定义格式:<类型说明符> <数组名> [<大小1>][<大小>]… 带有n个[<大小>]的为n维数组。方括号中的<大小>表示维数的大小,它也可以是一个常量表达式。
数组元素的表示:<数组名> [<下标表达式1>][<下标表达式2>]... 数组下标规定为从0开始并且各个元素在内存中是按下标的升序顺序存放的。
数组的赋值:数组可以被赋初值(及被初始化),也可以被赋值。在一般的情况下,只有在存储类为外部和静态的数组才可以被初始化。
数组的赋初值:这种情况实在定义或说明数组时实现的,实现赋初值的方法是使用初始值表,初始值表是用一对“{}”括起来的若干数据项组成的,多个数据项之间用逗号隔离,数据项的个数应该小于或等于数组元素的个数。
数组的赋值:这种情况是给数组中的各个元素赋值,其方法与一般变量的赋值方法相同,使用赋值达式语句
- 一维数组
1)定义
<元素类型> <一维数组变量名>[<元素个数>];
也可以借助 typedef 类定义
typedef <元素类型> <一维数组类型名>[<元素个数>]; <一维数组类型名> <一维数组变量名>
2)操作
通过下标访问元素。
注意下标是否越界。(C++中为了保证程序的执行效率不对下标越界进行检查,越界时可以运行,但是结果不可预测)
初始化
int a[3]={3,2,1}(或利用数组的赋值:int a[3];
a[0]=3;a[1]=2;a[2]=1;前一种利用的是数组的赋初值)
int是一维数组的数据类型,a是一维数组的数组名,3为一维数组的大小,它有三个元素,按照顺序分别为:
a[0]=3,
a[1]=2,
a[2]=1。
-
二维数组 1)定义 原理同一维数组
2)初始化 int a[2][3] = {{1,2,3},{4,5,6}}; 等同于 int a[2][3] = {1,2,3,4,5,6};//二维数组可以转成一维数组进行处理,但是要注意下标.
前一种格式是将数组a看成一维数组,它有两个元素,每个元素又可以看成一维数组)
int a[][3] = {{1,2},{3,4,5}};//第一个下标可以省略,其他的不能,更高维的数组也同此。
按行存储!
-
三维数组 1)定义
原理同一维数组
2)初始化
double c[2][3][2]={{{3,4},{5}},{{6},{7,8}}}(数组中有六个元素没有被初始化,它们都被默认为是0)
double是三维数组的数据类型,c是三维数组的数组名,12为三维数组的大小,它有十二个元素,按照顺序分别为: c[0][0][0]=3, c[0][0][1]=4, c[0][1][0]=0, c[0][1][1]=0, c[0][2][0]=5, c[0][2][1]=0, c[1][0][0]=6, c[1][0][1]=0, c[1][1][0]=7, c[1][1][1]=8, c[1][2][0]=0, c[1][2][1]=0。
-
字符数组 1)定义
字符数组是指数组元素是字符的一类数组。字符数组可以用来存放多个字符,也可以用来存放字符串,两种区别在前面已经申明过。
2) 字符数组存放的是字符还是字符串: 两者的区别在于数组元素中是否有字符串的结束符(’\0’)。
char s1[3]={‘a’,‘b’,‘c’} 存放的是字符
char s2[4]={‘a’,‘b’,‘c’,’\0’} 存放的是字符串,因此char s2[4]=“abc”。如果要对字符数组赋值时应对每一个元素进行赋值,不能用一个字符串常量直接赋值。(char s3[4];
s3[4]=“abc”;这种赋值是非法的,正确的应该为char s3[4];s3[0]=‘a’;s3[1]=‘b’;s3[2]=‘c’;s3[3]=’\0’😉
十、C++ 结构类型
struct inflatable
{
char name[20];
float volume;
double price;
};
inflatable hat;
hat.volume;
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
}
int main()
{
inflatable guest =
{
"Glorous Gloria",
1.88,
29.99
}
cout<< "Expand your guest list with "<<guest.name;
return 0;
}
结构类型用于表示由固定多个、类型可以不同的元素所构成的复合数据类型。
-
结构类型定义 struct <结构类型名> {<成员表>};
或 typedef struct <结构类型名> {<成员表>}<结构体类型别名>;
1)别名可以跟结构类型名不一样,但是一般都是一样的,设置别名是为了方便像其他变量类型一样定义变量,这是保留了C的语法。
2)在结构类型定义时,对成员变量进行初始化是没有意义的,因为类型不是程序运行时刻的实体,它们不占用内存空间。
-
结构类型变量定义 struct <结构类型名> <变量名表>;//C的用法
或 <结构类型名> <变量名表>;// C++的用法
或 struct <结构类型名> {<成员表>}<变量名表>;
-
操作 1)访问成员:<结构类型的变量名>.<成员名> 2)对结构类型的数据可以进行整体赋值,但是要保证两者属于相同的结构(成员名和类型都相同)。 -
存储 结构类型的变量在内存中占用一块连续的存储空间。 -
结构类型的默认参数传递方式是值传递,因此,当结构类型很大时传输速度回受限。 -
定义完结构类型后,其使用和平时的类型没有太大的区别,该加加该减减,不过要记住其每个成员也是一个实体。
#if 0
#include<iostream>
using namespace std;
main()
{
struct human {
char name[10];
int sex;
int age;
};
struct human x={"XieCh",1,21},*p=NULL;
p=&x;
cout<<"x.name="<<x.name<<endl;
cout<<"x.sex="<<x.sex<<endl;
cout<<"x.age="<<x.age<<endl;
cout<<"(*p).name="<<(*p).name<<endl;
cout<<"(*p).sex="<<(*p).sex<<endl;
cout<<"(*p).age="<<(*p).age<<endl;
cout<<"p->name="<<p->name<<endl;
cout<<"p->sex="<<p->sex<<endl;
cout<<"p->age="<<p->age<<endl;
cout<<"name:";
cin>>(*p).name;
cout<<"sex:";
cin>>(*p).sex;
cout<<"age:";
cin>>(*p).age;
cout<<"x.name="<<x.name<<endl;
cout<<"x.sex="<<x.sex<<endl;
cout<<"x.age="<<x.age<<endl;
}
#endif
更多的可参考这位博主的博客
十一、C++联合类型
联合类型(又称共同体类型),一种能够表示多种数据(类型可以相同可以不同,变量名字不同就行)的数据类型。 C++ 中的联合体是多个变量共享一段内存(相互覆盖),联合体的内存占用是所有成员中内存最大的那个所占用的大小。
1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。
-
联合类型的定义 union <联合类型名> {<成员表>}; 与结构类型类似,只是把struct 换成了 union. 在语义上,联合类型和结构类型的区别是,联合类型的所有成员占用同一块内存空间,该内存的空间大小是其最大成员的内存空间大小。 -
操作
#include using namespace std; union U { int n; char c[4]; }; int main() { U u; u.n = 0xa1a2a3a4; cout << "hex u.n = " << hex << u.n << " u.n address = " << &u.n << endl; cout << "u.c[0] = " << hex << (int)u.c[0] << " u.c[0] address = " << (void*)&u.c[0] << endl; cout << "u.c[1] = " << hex << (int)u.c[1] << " u.c[1] address = " << (void*)&u.c[1] << endl; cout << "u.c[2] = " << hex << (int)u.c[2] << " u.c[2] address = " << (void*)&u.c[2] << endl; cout << "u.c[3] = " << hex << (int)u.c[3] << " u.c[3] address = " << (void*)&u.c[3] << endl; system(“pause”); return 0; }
输出结果为
hex u.n = a1a2a3a4 u.n address = 00D3FCA4 u.c[0] = ffffffa4 u.c[0] address = 00D3FCA4 u.c[1] = ffffffa3 u.c[1] address = 00D3FCA5 u.c[2] = ffffffa2 u.c[2] address = 00D3FCA6 u.c[3] = ffffffa1 u.c[3] address = 00D3FCA7 请按任意键继续. . .
可以看到 c[0]里存储的是0xa4这个字符,c[1]里存储的是0xa3,c[2]里存储的是0xa2,c[3]里存储的是0xa1
采用的是小端模式存储
#include <iostream>
using namespace std;
union U1
{
int n;
char s[12];
double d;
};
union U2
{
int n;
char s[5];
double d;
};
int main()
{
U1 u1;
U2 u2;
cout << "sizeof(u1) : " << sizeof(u1) << "sizeof(U1) : " << sizeof(U1) << endl;
cout << "sizeof(u2) : " << sizeof(u2) << "sizeof(U2) : " << sizeof(U2) << endl;
cout << "u1的地址:" << &u1 << "\nu1.n的地址:" << &u1.n << "\nu1.s的地址:" << &u1.s
<< "\nu1.d的地址:" << &u1.d << endl;
cout << "u2的地址:" << &u2 << "\nu2.n的地址:" << &u2.n << "\nu2.s的地址:" << &u2.s
<< "\nu2.d的地址:" << &u2.d << endl;
system("pause");
return 0;
}
运行结果 sizeof(u1) : 16sizeof(U1) : 16 sizeof(u2) : 8sizeof(U2) : 8 u1的地址:002DF70C u1.n的地址:002DF70C u1.s的地址:002DF70C u1.d的地址:002DF70C u2的地址:002DF6FC u2.n的地址:002DF6FC u2.s的地址:002DF6FC u2.d的地址:002DF6FC 请按任意键继续. . .
可以看到 U1的大小是16,U1中最大成员占用的内存是12个字节,但是12不能被double类型8个字节整除,所以这里U1要占用16个字节。可以看到联合体中的元素都是占用的同一块存储区域,起始地址都是同一个地址 更多操作参考这篇博客
- 联合类型除了可以实现用一种类型表示多种类型的数据外,还可以实现多个数据共享内存空间,从而节省了内存空间。所以,当一些大型的数组变量,当它们的使用分布在程序的各个阶段时(不是同时使用),就可以使用联系类型来描述。
十二、C++ 指针类型
指针,用来描述内存地址,并通过提供指针操作来实现与内存相关的程序功能。
-
定义 <类型>* <指针变量>; 类型决定了指向的内存空间的大小。 指针变量也是一种变量,有着自己的内存空间,该空间上存储的是另一个变量的内存空间。 可以使用typedef取别名来减少定义变量时的一些麻烦,如typedef int* Pointer; -
操作 1)取地址 ‘&’
int* p; int x; p = &x;//p指向x的地址,p的类型是int*, &x的类型也是int*
2)间接访问 对于一般的指针变量,访问格式是:*<指针变量>
结构类型的指针变量,访问格式是:(*<指针变量>).<结构成员> 或 <指针变量>-><结构成员>
3)赋值 任何类型的指针都能赋给void *类型的指针变量,而非void * 类型的指针变量只能接受同类型的赋值。
4)指针运算 一个指针加上或减去一个整型值:<数据类型>* <指针变量>; int a; <指针变量>+a;可以理解为数组中下标的变化,
<指针变量> = <指针变量>+(a*sizeof(<数据类型>)) 两个同类型的指针相减:结果是整型值,对应的是存储空间中元素的个数,可用于求数组的大小。
两个同类型的指针比较:比较的是存储内容(也就是内存地址)的大小,可用于数组求和等。
5)指针的输出
非char *类型的指针变量:cout<<p;//输出的是p存储内容(内存地址) cout<<*p;//输出的是p存储内容上的内容
char *类型的指针变量:cout<<p;//输出的是以p为起始地址的字符串,这种用法在字符串变化中很常见 cout<<*p;//输出的是p上的一个字符
- 指向常量的指针变量
const <类型> *<指针变量>; 含义:不能改变指向的地址上的值(无论该地址上是常量还是变量),但是该变量的值是可以改变的。
?
-
指针与动态变量 动态变量是在程序运行时才产生,并在程序结束前消亡。动态变量跟局部变量不同,在程序运行前编译程序就知道了局部变量的存在 创建:
new <类型名>; 如:int *p; p=new int; *p=1;
new <类型名>[<整型表达式1>]...[<整型表达式n>]; 如:int (*q)[20]; int n=5;q=new int[n][20];
void *malloc(unsigned int size); 如:double *q; int n=2; q=(double *)malloc(sizeof(double)*n);
撤销:因为动态变量不能自动消亡,需要显示撤销其内存空间。
delete <指针变量>; 如:int *p=new int; delete p;
delete []<指针变量>; 如:int *p=new int[20]; delete []p;
void free(void *p); 如:int *p=(int *)malloc(sizeof(int)*6)
应用:动态数组、链表
-
指针 VS 无符号整数 指针从形式上看属于无符号数,但是指针可以关联到程序实体(变量或函数),指针指向某个内存地址,无符号整数的某些运算不能实施在指针上(如乘法和除法就不能)。 -
new VS malloc 1)new 自动计算所需分配的空间大小,而malloc需要显示指出。 2)new自动返回相应类型的指针,而malloc要做强制类型转换。 3)new会调用相应对象类的构造函数,而malloc不会。 对于new 和 malloc,如果程序的堆区没有足够的空间可供分配,则产生bad_alloc异常(会返回空指针NULL)。 -
delete VS free 1)delete会调用析构函数,free不会。 2)delete或free一个指针时,其实只是从编译器释放了这个地址的内存,但指针仍然指向该地址,此时的指针叫做悬浮指针,悬浮指针不为空指针,依据可以用来赋值或者和使用,所以会产生语义错误。(怎么解决呢?在delete或free后将指针设置为NULL)。 3)如果没有进行delete或free操作,就将指针指向别处,之前分配的内存空间就会一直存在但不能再被使用,也就是说造成了内存泄漏。 -
函数指针 函数指针就是指向函数的指针。 定义格式:<返回类型> (*<指针变量>)(<形式参数表>); 如:double (*fp)(int); double fun(int x); fp=&fun; 或 typedef <返回类型> (*<函数指针类型名>)(<形式参数表>); <函数指针类型名> <指针变量>; 如:typedef double (*FP)(int); FP fp;
使用:(*<指针变量>)(<形式参数表>); 如 (*fp)(10); 相当于 fun(10); 为什么使用函数指针:可以实现多态,一个函数指针指向不同的函数就可以实现不同的功能,可以结合设计模式理解。 可以向函数传递函数,如:int func(int (*fp)(int)){};
-
指针与数组 对数组元素的访问通常是通过下标来实现的,但是频繁地采用这种方式,有时效率不高,因为每次访问都要先通过下标计算数组元素的地址(就是上面的提到的指针变量加上一个整型数)。 -
多级指针 指针除了可以指向一般类型的变量外,还可以指向指针类型的变量。指针变量要初始化后才能使用。 如果一个指针变量没有初始化或者赋值,访问它所指向的变量将会导致运行时刻的严重错误。
int x;
int *p;
int **q;
*p=1;
*q=&x;
q=&p;
**q=2;
-
指向常量的指针类型 VS 指针类型的常量 指向常量的指针类型:不能改变指向的内容。如:const int *p; 指针类型的常量:指向的地址不能发生改变,内容可以改变,但是必须要初始化。如 int x; int *const q=&x; 两者结合:const int*const r;
本章节参考博文 https://blog.csdn.net/haitaolang/article/details/70156839?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162892493216780255260866%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162892493216780255260866&biz_id=0&spm=1018.2226.3001.4187
十三、C++ 引用类型
- 什么叫引用,通俗点来说,就是一个人的外号或者叫做小名
- 我们为什么要使用引用?
使用引用类型就不必在函数中声明形参是指针变量。指针变量要另外开辟内存单元,其内容是地址。而引用变量不是一个独立的变量,不单独占内存单元,达到节省内存空间的作用。
int a = 20;
int& b = a;
b = 10;
cout << a << endl;
定义时必须要初始化
int a;
int* p;
a = 20;
p = &a;
int& b;
int& b = a;
如: int x=0;
int &y=x;
y=2;
- 引用一旦初始化就不能再引用其它变量。
int a = 20, c = 30;
int& b = a;
b = c;
- 引用的应用场景
1)引用型参数 a.修改实参 b.避免拷贝,通过加const可以防止在函数中意外地修改实参的值,同时还可以接受拥有常属性的实参。 2)引用型返回值
int b = 10;
int a = func (b);
func (b) = a;
从一个函数中返回引用往往是为了将该函数的返回值作为左值使用。但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定的后果。 不要返回局部变量的引用,可以返回全局、静态、成员变量的引用,也可以返回引用型形参变量本身。
- 结构体类型的引用
#include <iostream>
using namespace std;
typedef struct Coor
{
int x;
int y;
};
int main(){
Coor c1;
Coor &c2 = c1;
c2.x = 10;
c2.y = 20;
cout<<c1.x<<" "<<c1.y<<endl;
system("pause");
return 0;
}
- 指针类型的引用
类型 *&指针引用名 = 指针;
#include <iostream>
using namespace std;
int main(){
int a = 10;
int *p = &a;
int *&q = p;
*q= 20;
cout<<a<<endl;
system("pause");
return 0;
}
- 引用作函数参数
void fun( int *a,int *b)
{
int temp = 0;
c = *a;
*a = *b;
*b = c;
}
int x = 10,y = 20;
fun(&x,&y);
使用引用参数可以直接操作实参变量,从而能够实现通过修改形参的值而达到修改对应实参值得目的。当引用作为函数形参,其引用的目标变量没人为调用该函数时对应的实参变量名,所以,在定义函数时,对于引用类型参数不必提供引用的初始值。
十四、C++ 成员的访问控制
首先解释几个特定词,下面要用到:
水平权限:在一个类中,成员的权限控制,就是类中的成员函数能否访问其他成员、类的对象能否访问类中某成员。 垂直权限:在派生类中,对从基类继承来的成员的访问。 内部访问:类中成员函数对其他成员的访问。 外部访问:通过类的对象,访问类的成员函数或者成员变量,有的书里也称之为对象访问。 C++的水平权限控制 当private,public,protected单纯的作为一个类中的成员(变量和函数)权限设置时: 类的成员函数以及友元函数可以访问类中所有成员,但是在类外通过类的对象,就只能访问该类的共有成员。 注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数;这里将友元函数看成内部函数,方便记忆!
总结为下表: 
程序验证如下:这里没有friend的成员,另外成员变量和成员函数的权限控制是一样的。
#include <iostream>
class Foo
{
public:
int a;
void public_fun();
protected:
char b;
void protected_fun();
private:
bool c;
void private_fun();
};
void Foo::public_fun()
{
a = 10;
b = 'c';
c = true;
}
void Foo::protected_fun()
{
a = 10;
b = 'c';
c = true;
}
void Foo::private_fun()
{
a = 10;
b = 'c';
c = true;
}
int main()
{
Foo foo;
foo.public_fun();
foo.protected_fun();
foo.private_fun();
return 0;
}
C++的垂直访问控制 当private,public,protected作为继承方式时: 派生类可以继承基类中除了构造函数与析构函数(凡是与具体对象的类别息息相关的都不能被继承,赋值运算符重载函数也不能被继承)之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性是由继承方式控制的。 总结为下表:  派生类对基类成员的访问形式主要有以下两种:
内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
本章节参考[博客](https://blog.csdn.net/weixin_42018112/article/details/82427071?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162892620716780366583388%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162892620716780366583388&biz_id=0&spm=1018.2226.3001.4187)https://blog.csdn.net/weixin_42018112/article/details/82427071?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162892620716780366583388%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162892620716780366583388&biz_id=0&spm=1018.2226.3001.4187 该博主写的很棒,借此引用。
十五、C++ 构造和析构函数
学习这部分前,我们再重新温习一下类的基本语法。
- 类的定义
class 类名 { }; 如 class Student { }; - 成员变量——属性
class 类名 { 类型 成员变量名; }; 如 class Student { string m_name; int m_age; }; - 成员函数——行为
class 类名 { 返回类型 成员函数名 (形参表) { 函数体; } }; 如
class Student {
string m_name;
int m_age;
void eat (const string& food) {
...
}
};
4.访问控制属性 1)公有成员:public,谁都可以访问。 2)私有成员:private,只有自己可以访问。 3)保护成员:protected,只有自己和自己的子类可以访问。 4)类的成员缺省访控属性为私有,而结构的成员缺省访控属性为公有。 例子:
#include <iostream>
using namespace std;
class Student {
private:
string m_name;
int m_age;
public:
void eat (const string& food) {
cout << m_age << "岁的" << m_name
<< "同学正在吃" << food << "。" << endl;
}
void setName (const string& name) {
if (name == "2")
cout << "你才" << name << "!" << endl;
else
m_name = name;
}
void setAge (int age) {
if (age < 0)
cout << "无效的年龄!" << endl;
else
m_age = age;
}
};
int main (void) {
Student student;
student.setName ("2");
student.setAge (-100);
student.setName ("张飞");
student.setAge (20);
student.eat ("包子");
return 0;
}
【构造函数】
- C++中的类需要定义与类名相同的特殊成员函数时,这种与类名相同的成员函数叫做构造函数;
- 构造函数可以在定义的时候有参数;
- 构造函数没有任何返回类型。
- 构造函数的调用: 一般情况下,C++编译器会自动的调用构造函数。特殊情况下,需要手工的调用构造函数。
class Test
{
public:
Test()
{
}
}
【析构函数】
- C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的函数是析构函数;
- 析构函数没有参数和没有任何返回类型;
- 析构函数在对象销毁的时候自动调用;
- 析构函数调用机制: C++编译器自动调用。
class Test
{
~Test()
{
}
}
可以说,构造函数和析构函数就是双一对胞胎,由代码可以看出来,他们两长得十分相似。而且这两个是一对"生死冤家“,为什么这么说呢,因为他们俩总是一同存在,一起消亡,同生共死!
让我们看一段代码来更深刻了解一下这对生死冤家吧
class Test
{
private:
int my_a;
public:
Test()
{
my_a = 1;
}
Test(int a)
{
my_a = a;
}
Test(const Test& obj)
{
}
~Test(){}
};
void main()
{
Test t1;
Test t2(1);
Test t3();
Test t4 = 2;
Test t5 = Test(1);
return;
}
这边推荐大家在自己电脑上在运行一下下面这段代码,真实的看看构造函数和析构函数的运行过程和结果!
class Test
{
private:
int x;
public:
Test(int x)
{
this->x = x;
cout << "对象被创建" << endl;
}
Test()
{
x = 0;
cout << "对象被创建" << endl;
}
void init(int x)
{
this->x = x;
cout << "对象被创建" << endl;
}
~Test()
{
cout << "对象被释放" << endl;
}
int GetX()
{
return x;
}
};
int main()
{
Test a(10);
Test b;
b.init(10);
Test arr[3] = {Test(10),Test(),Test()};
Test brr[3];
cout<<brr[0].GetX()<<endl;
brr[0].init(10);
cout<<brr[0].GetX()<<endl;
return 0;
}
十六、C++ this指针
每一个成员函数(静态成员函数除外)都有一个this隐藏的指针类型的形参this,其类型为: <类型> *const this; 话是这么说的,但this指针又是什么呢?
说来也简单,this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。 所谓当前对象,是指正在使用的对象。例如对于 stu.show();,stu 就是当前对象,this 就指向 stu。
下面是使用 this 的一个完整示例:
#include <iostream>
using namespace std;
class Student {
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name) {
this->name = name;
}
void Student::setage(int age) {
this->age = age;
}
void Student::setscore(float score) {
this->score = score;
}
void Student::show() {
cout << this->name << "的年龄是" << this->age << ",成绩是" << this->score << endl;
}
int main() {
Student *pstu = new Student;
pstu->setname("李华");
pstu->setage(16);
pstu->setscore(96.5);
pstu->show();
return 0;
}
注意,this 是一个指针,要用->来访问成员变量或成员函数。
说什么?你看不懂?那就给你们看个简易版的
class A
{
int x,y;
public:
void f();
void g(int x){
this->x = x;
}
}
这个是不是就显然易懂了呢。
- this 到底是什么
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。 this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。 在《 C++函数编译原理和成员函数的实现》一节中讲到,成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。 这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。
让我们用三个例子,在深刻熟悉this指针的同时,重新复习一下上一章节构造和析构函数的使用方法吧!
例子1:
#include <iostream>
using namespace std;
class A {
public:
A (int data) : data (data) {
cout << "构造: " << this << endl;
}
void foo (void) {
cout << "foo: " << this << endl;
cout << this->data << endl;
}
int data;
};
int main (void) {
A a (1000);
cout << "main: " << &a << endl;
a.foo ();
A* pa = new A (1000);
cout << "main: " << pa << endl;
pa->foo ();
delete pa;
}
例子2:
#include <iostream>
using namespace std;
class Counter {
public:
Counter (void) : m_data (0) {}
Counter& inc (void) {
++m_data;
return *this;
}
void print (void) {
cout << m_data << endl;
}
private:
int m_data;
};
int main (void) {
Counter c;
c.inc ().inc ().inc ();
c.print ();
return 0;
}
例子3:学生与老师
#include <iostream>
using namespace std;
class Student;
class Teacher {
public:
void educate (Student* s);
void reply (const string& answer) {
m_answer = answer;
}
private:
string m_answer;
};
class Student {
public:
void ask (const string& question, Teacher* t) {
cout << "问题:" << question << endl;
t->reply ("不知道。");
}
};
void Teacher::educate (Student* s) {
s->ask ("什么是this指针?", this);
cout << "答案:" << m_answer << endl;
}
int main (void) {
Teacher t;
Student s;
t.educate (&s);
return 0;
}
十七、C++ 拷贝构造函数
class A
{
int x,y;
public:
A();
A(const A& a)
{
x = a.x+1;
y = a.y+1;
}
}
其中,const是为了防止在函数中修改实参对象,可以省略。 拷贝构造函数也可以带有其他参数,但这些参数必须要有默认值。
- 复制构造函数(英语:Copy constructor)是C++编程语言中的一种特别的构造函数,习惯上用来创建一个全新的对象,这个全新的对象相当于已存在对象的副本。这个构造函数只有一个参数(引数):就是用来复制对象的引用(常用const修饰)。构造函数也可以有更多的参数,但除了最左第一个参数是该类的引用类型外,其它参数必须有默认值。
类的复制构造函数原型通常如下:
Class_name(const Class_name & src);
一般来说,假如程序员没有自行编写复制构造函数,那么编译器会自动地替每一个类创建一个复制构造函数;相反地,程序员有自行编写复制构造函数,那么编译器就不会创建它。
当对象包括指针或是不可分享的引用时,程序员编写显式的复制构造函数是有其必要性的,例如处理文件的部分,除了复制构造函数之外,应该还要再编写析构函数与赋值运算符的部分,也就是三法则。
- 调用
- 下面三种情况将会调用拷贝构造函数:
1)定义对象 2)把对象作为值参数传递给函数 3)把对象作为返回值 如果在类定义中没有给出拷贝构造函数,则编译程序将会为其提供一个隐式的拷贝构造函数,此时的拷贝构造函数跟Java中的克隆函数有点像。 当类定义中包含成员对象,成员对象的拷贝初始化可由成员对象类的拷贝构造函数来实现。 系统提供的隐式拷贝构造函数会去调用成员对象的拷贝构造函数,而自定义的拷贝构造函数则不会自动去调用成员的拷贝构造函数,这时,必须要在拷贝构造函数的成员初始化表中显式指出。
口说无凭,让我们看看例子吧
#include <iostream>
using namespace std;
class Integer {
public:
Integer (int data = 0) : m_data (data) {}
void print (void) const {
cout << m_data << endl;
}
Integer (const Integer& that) :
m_data (that.m_data) {
cout << "拷贝构造" << endl;
}
private:
int m_data;
};
void foo (Integer i) {
i.print ();
}
Integer bar (void) {
Integer i;
return i;
}
int main (void) {
Integer i1 (10);
i1.print ();
Integer i2 (i1);
i2.print ();
Integer i3 = i1;
i3.print ();
cout << "调用foo()函数" << endl;
foo (i1);
cout << "调用bar()函数" << endl;
Integer i4 (bar ());
return 0;
}
由[实例],得拷贝构造函数心得
拷贝赋值运算符函数
形如 class X {
X& operator= (const X& that) {
...
}
};的成员函数称为拷贝赋值运算符函数。
- 如果一个类没有定义拷贝赋值运算符函数,系统会提供一个缺省拷贝赋值运算符函数。缺省拷贝赋值运算符函数对于基本类型的成员变量,按字节复制,对于类类型的成员变量,调用相应类型的拷贝赋值运算符函数。
- 在某些情况就下,缺省拷贝赋值运算符函数只能实现浅拷贝,如果需要获得深拷贝的复制效果,就需要自己定义拷贝赋值运算符函数。
例子:拷贝赋值运算符函数
#include <iostream>
using namespace std;
class Integer {
public:
Integer (int data) : m_data (new int (data)) {}
~Integer (void) {
if (m_data) {
delete m_data;
m_data = NULL;
}
}
void print (void) const {
cout << *m_data << endl;
}
Integer (const Integer& that) :
m_data (new int (*that.m_data)) {}
void set (int data) {
*m_data = data;
}
Integer& operator= (const Integer& that) {
if (&that != this) {
delete m_data;
m_data = new int (*that.m_data);
}
return *this;
}
private:
int* m_data;
};
int main (void) {
Integer i1 (10);
i1.print ();
Integer i2 (i1);
i2.print ();
i2.set (20);
i2.print ();
i1.print ();
Integer i3 (30);
i3.print ();
i3 = i1;
i3.print ();
i3.set (40);
i3.print ();
i1.print ();
(i3 = i1) = i2;
i3.print ();
i3 = i3;
i3.print ();
return 0;
}
十八、C++ 常成员函数
声明:<类型标志符>函数名(参数表)const;
说明:
(1)const是函数类型的一部分,在实现部分也要带该关键字。
(2)const关键字可以用于对重载函数的区分。
(3)常成员函数不能更新类的成员变量,也不能调用该类中没有用const修饰的成员函数,只能调用常成员函数。
① 首先,常成员函数内部不允许进行数据成员的修改,但是可以在函数内部输出const数据成员与非数据成员!
② 其次,还可以区分同名构造函数,举个例子(如下):
#include <iostream>
using namespace std;
class Test
{
public:
void Show()const
{
cout << "Hello,Const!" << endl;
}
void Show()
{
cout << "Hello!" << endl;
}
};
int main()
{
Test t1;
t1.Show();
Test const t2;
t2.Show();
return 0;
}
再让我们看一例子
#include<iostream>
using namespace std;
class IntCell
{
public:
explicit IntCell(int x):a(x)
{}
int read() const
{
return a;
}
private:
int a;
};
int main()
{
IntCell obj(3);
cout<<obj.read()<<endl;
return 0;
}
我们可以看到read()函数,括号后有一个const,这是为什么呢? read()函数的主要功能是返回成员变量,所以并没有对该类进行改变,所以这个函数即为常成员函数,我们要在函数定义时在括号后加上const这个关键词说明它是常成员函数,这是一个很好的编程习惯。
其实简单的说,常数据成员、常成员函数和常对象这些有常的,都是加上const进行修饰实例的。
1、常成员函数可以被其他成员函数调用。
2、但是不能调用其他非常成员函数。
3、可以调用其他常成员函数。
十九、C++ 静态成员和静态函数
class A
{
public:
static int x;
public:
static void Func(){}
}
int A::x=10;
类的静态成员主要是用来解决资源共享的问题。说简单也简单,说难也挺难的,那就让我们简单了解一下吧。
在C++中,采用静态成员来解决同一个类的对象共享数据的问题。类的静态成员分为静态数据成员和静态成员函数。
- 静态数据成员:
类中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。 静态数据成员在一个类中只分配一次存储空间,也就是一个类的所有对象的静态数据成员共享一块存储空间。 在计数时往往使用的就是静态数据成员。 【定义】静态数据成员实际上是类域中的全局变量。
静态数据成员的定义(初始化)不应该被放在头文件中。其定义方式
与全局变量相同。如下:
xxx.h文件
lass A
{
private:
static int x;
};
xxx.cpp文件
int base::x=10;
注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使
加上 #ifndef #define #endif 或者 #pragma once 也不行。
- 静态数据成员被类的所有对象所共享,包括该类派生类的对象
- 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以
class base{
public :
static int _staticVar;
int _var;
void foo1(int i=_staticVar);
void foo2(int i=_var);
};
这是因为实例成员的存储空间属于具体的实例,不同实例(对象)的同名成员拥有不同的存储空间;静态成员的存储
空间是固定的,与具体的实例(对象)无关,被该类的所有实例共享。
4.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为所属类类型的指针或引用
二:静态函数成员
1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用类成员函数指针来储存
class base
{
static int func1();
int func2();
};
int (*pf1)()=&base::func1;
int (base::*pf2)()=&base::func2;
2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。但是非静态成员函数可以调用静态成员
3.静态成员函数不可以同时声明为 virtual、const、volatile函数 4.静态成员函数只能访问静态成员(包括静态数据成员和静态成员函数),并且静态成员的访问也要遵循类的访问控制。 静态成员函数没有隐藏的this指针参数,因为静态成员函数对静态数据成员进行操作,而静态数据成员是某类对象共享的,它们只有一个拷贝,因此,静态成员函数不需要知道某个具体对象。
三:其它一些要注意的事项
1.静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问
2.访问静态成员可以用ClassName::MemberName,也可以用ClassName.MemberName,当然,为了和非静态成员区 分一下,建议使用前者。访问实例成员只能用ClassName.MemberName,不能用ClassName::MemberName。 **
**
例子0:
#include <iostream>
using namespace std;
class A {
public:
static int m_i;
static void foo (void) {
cout << "foo:" << m_i << endl;
}
double m_d;
void bar (void) {
m_i = 1000;
foo ();
}
};
int A::m_i = 1;
int main (void) {
A::m_i = 10;
A a1, a2;
cout << ++a1.m_i << endl;
cout << a2.m_i << endl;
A::foo ();
a1.foo ();
a1.bar ();
return 0;
}
例子1:单例模式(饿汉方式):
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton& getInst (void) {
return s_inst;
}
private:
Singleton (void) {}
Singleton (const Singleton&);
static Singleton s_inst;
};
Singleton Singleton::s_inst;
int main (void) {
Singleton& s1 = Singleton::getInst ();
Singleton& s2 = Singleton::getInst ();
Singleton& s3 = Singleton::getInst ();
cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
return 0;
}
例子2:单例模式(懒汉模式)
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton& getInst (void) {
if (! m_inst)
m_inst = new Singleton;
++m_cn;
return *m_inst;
}
void releaseInst (void) {
if (m_cn && --m_cn == 0)
delete this;
}
private:
Singleton (void) {
cout << "构造:" << this << endl;
}
Singleton (const Singleton&);
~Singleton (void) {
cout << "析构:" << this << endl;
m_inst = NULL;
}
static Singleton* m_inst;
static unsigned int m_cn;
};
Singleton* Singleton::m_inst = NULL;
unsigned int Singleton::m_cn = 0;
int main (void) {
Singleton& s1 = Singleton::getInst ();
Singleton& s2 = Singleton::getInst ();
Singleton& s3 = Singleton::getInst ();
cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
s3.releaseInst ();
s2.releaseInst ();
s1.releaseInst ();
return 0;
}
部分原文链接:https://blog.csdn.net/bruce_zeng/article/details/8173506
二十、C++ 继承
终于到了继承这一部分了,说道继承就不得不说说父子这个不恰当的例子,父亲的所有东西,都是儿子的东西,儿子都可以拿来使用,而儿子的东西,却不一定是父亲的。这也就是最简单的继承
那继承的概念是什么呢? 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有的特性基础上进行扩展,增加功能,这样产生新的类,称作是派生类。继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。 继承是类的重要特性。 A类继承B类,我称B类为“基类”,A为“子类”。A类继承了B类之后,A类就具有了B类的部分成员,具体得到了那些成员,这得由两个方面决定:
class Person
{
public:
void Print(){
cout<<"name:"<<_name<<endl;
cout<<"age:"<<_age<<endl;
}
protected:
string _name = "Romeo";
int _age = 18;
};
class Student: public Person
{
protected:
int _stuid;
};
class Teacher:public Person
{
protected:
int _jobid;
};
class Student:public Person
{
public:
int _stuid;
char _major;
};
Student称为 派生类; 第一行的public是继承方式; Person称为基类。
再看一个例子
#include <iostream>
using namespace std;
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
输出得 Total area: 35
-
让我们来具体看看C++ 有哪些继承方式 -
public 1)基类的public成员,在派生类中成员public成员 2)基类的protected成员,在派生类中成员protected成员 3)基类的private成员,在派生类中成员不可直接使用的成员 -
protected 1)基类的public成员,在派生类中成员protected成员 2)基类的protected成员,在派生类中成员protected成员 3)基类的private成员,在派生类中成员不可直接使用的成员 -
private 1)基类的public成员,在派生类中成员private成员 2)基类的protected成员,在派生类中成员private成员 3)基类的private成员,在派生类中成员不可直接使用的成员 用图像表示就是如下图  -
在任何继承方式中,除了基类的private成员外,我们都可以在派生类中分别调整其访问控制,如下述例子可得
class A
{
public:
void f1();
void f2();
void f3();
protected:
void g1();
void g2();
void g3();
}
class B: private A
{
public:
A::f1;
A::g1;
protected:
A::f2;
A::g2;
}
class C: class B
{
public:
void h()
{
f1(); f2(); g1(); g2();
f3(); g3();
}
}
调整的格式一般都是 [public: | protected: | private: ] <基类名>:: <基类成员名>; 
- 派生类的构造
- 派生类是可以访问基类保护的数据成员,但是还有一些私有数据成员,派生类是无法访问的,并且为提醒类的独立性,我们还是希望通过调用基类的成员函数去初始化这些成员变量,所以派生类是通过调用基类的构造函数,实现对成员变量的初始化。具体代码示例,见上。
子类的构造函数和析构函数
- 子类隐式地调用基类的构造函数
在子类的构造函数中没有显示地指明其基类部分如何构造,隐式地调用基类的无参构造函数。如果子类没有定义任何构造函数,其缺省无参构造函数同样会隐式地调用基类的无参构造函数。 - 子类显式地调用基类的构造函数
在子类构造函数的初始化表中指明其基类部分的构造方式。
class A {
public:
A (void) : m_data (0) {}
A (int data) : m_data (data) {}
private:
int m_data;
};
class B : public A {
public:
B (int data) : A (data) {}
};
class A { ... };
class B : public A { ... };
class C : public B { ... };
C c (...);
构造:A->B->C 析构:C->B->A3.继承链的构造和初始化顺序 任何时候子类中基类子对象的构造都要先于子类构造函数中的代码。
- delete一个指向子类对象的基类指针,实际被执行的基类的析构函数,基类的析构函数不会调用子类析构函数,因此子类所特有的资源将形成内存泄漏。
Human* p = new Student (…); delete p; // ->Human::~Human() delete static_cast<Student*> §;
用于防止或者限制基类中的公有接口被从子类中扩散。
class DCT {
public:
void codec (void) { ... }
};
class Jpeg : protected DCT {
public:
void render (void) {
codec (...);
}
};
Jpeg jpeg;
jpeg.codec (...);
Jpeg Has A DCT,实现继承
class Jpeg2000 : public Jpeg {
public:
void render (void) {
codec (...);
}
};
例子:
#include <iostream>
using namespace std;
class Human {
public:
Human (const string& name, int age) :
m_name (name), m_age (age) {}
void who (void) const {
cout << m_name << "," << m_age << endl;
}
void eat (const string& food) const {
cout << "我在吃" << food << endl;
}
protected:
string m_name;
int m_age;
};
class Student : public Human {
public:
Student (const string& name, int age, int no) :
Human (name, age), m_no (no) {}
错误的构造函数会报错,原因是这里调用Human的构造函数时没有指定方式,默认使用无参构造,但是在基类Human类中没有无参构造,所以会报错
Student (const Student& that) :
Human (that), m_no (that.m_no) {}
Student& operator= (const Student& that) {
if (&that != this) {
Human::operator= (that);
m_no = that.m_no;
}
return *this;
}
void learn (const string& lesson) const {
cout << "我(" << m_name << "," << m_age
<< "," << m_no << ")在学" << lesson
<< endl;
}
using Human::eat;
void eat (void) const {
cout << "我绝食!" << endl;
}
private:
int m_no;
};
int main (void) {
Student s1 ("张飞", 25, 1001);
s1.who ();
s1.eat ("包子");
s1.learn ("C++");
Human* h1 = &s1;
h1 -> who ();
h1 -> eat ("KFC");
Student* ps = static_cast<Student*> (h1);
ps -> learn ("C");
Student s2 = s1;
s2.who ();
s2.learn ("英语");
Student s3 ("赵云", 20, 1002);
s3 = s2;
s3.who ();
s3.learn ("数学");
return 0;
}
- 私有继承和保护继承
用于防止或者限制基类中的公有接口被从子类中扩散。
class DCT {
public:
void codec (void) { ... }
};
class Jpeg : protected DCT {
public:
void render (void) {
codec (...);
}
};
Jpeg jpeg;
jpeg.codec (...);
class Jpeg2000 : public Jpeg {
public:
void render (void) {
codec (...);
}
};
**
** 从多于一个基类中派生子类。 电话 媒体播放器 计算机 \ | / 智能手机
- 多重继承的语法和语义与单继承并没有本质的区别,只是子类对象中包含了更多的基类子对象。它们在内存中按照继承表的先后顺序从低地址到高地址依次排列。
- 子类对象的指针可以被隐式地转换为任何一个基类类型的指针。无论是隐式转换,还是静态转换,编译器都能保证特定类型的基类指针指向相应类型基类子对象。但是重解释类型转换,无法保证这一点。
- 尽量防止名字冲突。
例子:智能手机
#include <iostream>
using namespace std;
class Phone {
public:
Phone (const string& numb) : m_numb (numb) {}
void call (const string& numb) {
cout << m_numb << "致电" << numb << endl;
}
void foo (void) {
cout << "Phone::foo" << endl;
}
private:
string m_numb;
};
class Player {
public:
Player (const string& media) : m_media (media){}
void play (const string& clip) {
cout << m_media << "播放器播放" << clip
<< endl;
}
void foo (int data) {
cout << "Player::foo" << endl;
}
private:
string m_media;
};
class Computer {
public:
Computer (const string& os) : m_os (os) {}
void run (const string& prog) {
cout << "在" << m_os << "上运行" << prog
<< endl;
}
private:
string m_os;
};
class SmartPhone : public Phone, public Player,
public Computer {
public:
SmartPhone (const string& numb,
const string& media, const string& os) :
Phone (numb), Player (media),
Computer (os) {}
using Phone::foo;
using Player::foo;
};
int main (void) {
SmartPhone sp ("13910110072", "MP3", "Android");
sp.call ("01062332018");
sp.play ("High歌");
sp.run ("愤怒的小鸟");
Phone* p1 = reinterpret_cast<Phone*> (&sp);
Player* p2 = reinterpret_cast<Player*> (&sp);
Computer* p3 = reinterpret_cast<Computer*>(&sp);
cout << &sp << ' '<< p1 << ' ' << p2 << ' '
<< p3 << endl;
sp.foo ();
sp.foo (100);
return 0;
}
1)钻石继承 A / B C \ / D class A { … }; class B : public A { … }; class C : public A { … }; class D : public B,public C{ …};
在最终子类(D)对象中存在公共基类(A)子对象的多份实例,因此沿着不同的继承路径访问公共基类子对象中的成员,会发生数据不一致的问题。
2)虚继承 在继承表中通过virtual关键字指定从公共基类中虚继承,这样就可以保证在最终子类对象中,仅存在一份公共基类子对象的实例,避免沿着不同的继承路径访问公共基类子对象中的成员时,所引发的数据不一致的问题。 只有当所创建对象的类型回溯(su)中存在钻石结构时,虚继承才起作用,否则编译器会直接忽略virtual关键字。
**例子:钻石继承和虚继承**
#include <iostream>
using namespace std;
class A {
public:
A (int i) : m_i (i) {}
protected:
int m_i;
};
class B : virtual public A {
public:
B (int i) : A (i) {}
void set (int i) {
m_i = i;
}
};
class C : virtual public A {
public:
C (int i) : A (i) {}
int get (void) {
return m_i;
}
};
class D : public B, public C {
public:
D (int i) : B (i), C (i), A (i) {}
};
int main (void) {
D d (1000);
cout << d.get () << endl;
d.set (2000);
cout << d.get () << endl;
return 0;
}
二十一、C++多态
C++三特性:继承,封装,多态。继承和封装都有所接触的情况下,下一步就是了解多态了。
那什么是多态呢?
多态就是多种状态,就是在完成某个行为时,当不同的对象去完成时会有不同的状态。
多态实现的三个条件:
-
必须是公有继承 -
必须是通过基类的指针或引用 指向派生类对象 访问派生类方法 -
基类的方法必须是虚函数,且完成了虚函数的重写
多态分为静态多态(编译阶段)和动态多态(运行阶段)
- 静态多态:函数重载和泛型编程
- 动态多态:虚函数 :根据绑定的类型调用响应的函数执行!
动态多态依靠虚函数来实现:动态多态三要素:
- 父类有虚函数;
- 子类改写了虚函数;
- 通过父类的指针或引用来调用虚函数, 在运行时,绑定到不同的子类中,产生不同的行为
图形:位置,绘制 / 矩形:宽和高 圆:半径 绘制 绘制
直接上例子:图形绘制
#include <iostream>
using namespace std;
class Shape {
public:
Shape (int x, int y) : m_x (x), m_y (y) {}
virtual void draw (void) {
cout << "形状(" << m_x << ',' << m_y << ')'
<< endl;
}
protected:
int m_x, m_y;
};
class Rect : public Shape {
public:
Rect (int x, int y, int w, int h) :
Shape (x, y), m_w (w), m_h (h) {}
void draw (void) {
cout << "矩形(" << m_x << ',' << m_y << ','
<< m_w << ',' << m_h << ')' << endl;
}
private:
int m_w, m_h;
};
class Circle : public Shape {
public:
Circle (int x, int y, int r) :
Shape (x, y), m_r (r) {}
void draw (void) {
cout << "圆形(" << m_x << ',' << m_y << ','
<< m_r << ')' << endl;
}
private:
int m_r;
};
void render (Shape* shapes[]) {
for (size_t i = 0; shapes[i]; ++i)
shapes[i]->draw ();}
且调用时,有指针的指向的目标类型决定执行哪一个函数,真正执行的是覆盖版本的draw
所以就通过指针调用各自的draw(),这样就可以用各自的绘制方法画出图形;(有了virtual修饰,则是按照指针指向的对象来找draw)
但是如果没有在基类shape中的draw()没有用virtual修饰,则shape类型的指针会访问shape类中的draw,则全部会用基类shape中的draw绘制图形(是根据指针类型来找draw)
int main (void) {
Shape* shapes[1024] = {};
shapes[0] = new Rect (1, 2, 3, 4);
shapes[1] = new Circle (5, 6, 7);
shapes[2] = new Circle (8, 9, 10);
shapes[3] = new Rect (11, 12, 13, 14);
shapes[4] = new Rect (15, 16, 17, 18);
render (shapes);
return 0;
}
更明白的说:多态=虚函数+指针/引用
Rect rect (...);
Shape shape = rect;
shape->draw ();
Shape& shape = rect;
shape->draw ();
-------------------------------------------------
class A {
public:
A (void) {
bar ();
}
~A (void) {
bar ();
}
void foo (void) {
This->bar ();
}
virtual void bar (void) {
cout << 'A' << endl;
}
};
class B : public A {
void bar (void) {
cout << 'B' << endl;
}
};
int main (void) {
B b;
b.foo ();
return 0;
}
二十三、C++ 函数覆盖的条件
overload - 重载 override - 覆盖、重写、改写
- 基类版本必须是虚函数。
- 函数名、形参表和常属性必须严格一致。
- 如果返回基本类型或者对象,那么也必须严格一致。如果返回类类型的指针或引用,那么子类版本也可以返回基类版本的子类。
class B : public A { … }; 基类:virtual A* foo (void) {…} 子类:A* foo (void) { … } B* foo (void) { … } - 子类的覆盖版本不能比基类版本声明更多的异常抛出。
- 子类覆盖版本的访控属性与基类无关。
class A {
public:
virtual void foo (void) { ... }
};
class B : public A {
private:
void foo (void) { ... }
};
int main (void) {
B* b = new B;
b->foo ();
A* a = new B;
a->foo ();
}
二十二、C++ 纯虚函数
- 什么是虚函数?
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。 就像如下,定义一虚函数
#include <iostream>
using namespace std;
class baseType{
public:
virtual void print(){
cout<<"This is baseType"<<endl;
}
}
class dependentType: public baseType{
public:
virtual void print(){
cout<<"This is dependentType"<<endl;
}
}
即形如:
抽象类: 不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。 纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
class A {
virtual void foo (void) = 0;
virtual void bar (void) = 0;
virtual void fun (void) = 0;
};
class B : public A {
void foo (void) { ... }
};
class C : public B {
void bar (void) { ... }
};
class D : public C {
void fun (void) { ... }
};
除了构造和析构函数以外,所有的成员函数都是纯虚函数的类称为纯抽象类。 例子:
#include <iostream>
using namespace std;
class Shape {
public:
Shape (int x, int y) : m_x (x), m_y (y) {}
virtual void draw (void) = 0;
protected:
int m_x, m_y;
};
class Rect : public Shape {
public:
Rect (int x, int y, int w, int h) :
Shape (x, y), m_w (w), m_h (h) {}
void draw (void) {
cout << "矩形(" << m_x << ',' << m_y << ','
<< m_w << ',' << m_h << ')' << endl;
}
private:
int m_w, m_h;
};
class Circle : public Shape {
public:
Circle (int x, int y, int r) :
Shape (x, y), m_r (r) {}
void draw (void) {
cout << "圆形(" << m_x << ',' << m_y << ','
<< m_r << ')' << endl;
}
private:
int m_r;
};
void render (Shape* shapes[]) {
for (size_t i = 0; shapes[i]; ++i)
shapes[i]->draw ();
}
int main (void) {
Shape* shapes[1024] = {};
shapes[0] = new Rect (1, 2, 3, 4);
shapes[1] = new Circle (5, 6, 7);
shapes[2] = new Circle (8, 9, 10);
shapes[3] = new Rect (11, 12, 13, 14);
shapes[4] = new Rect (15, 16, 17, 18);
render (shapes);
return 0;
}
二十三、C++ 动态绑定(后期绑定、运行时绑定)
- 虚函数表
class A {
public:
virtual void foo (void) { ... }
virtual void bar (void) { ... }
};
class B : public A {
public:
void foo (void) { ... }
};
A* pa = new A;
pa->foo ();
pa->bar ();
---------------------
A* pa = new B;
pa->foo ();
pa->bar ();
- 动态绑定
当编译器看到通过指向子类对象的基类指针或者引用子类对象的基类引用,调用基类中的虚函数时,并不急于生成函数调用代码,相反会在该函数调用出生成若干条指令,这些指令在程序的运行阶段被执行,完成如下动作: 1)根据指针或引用的目标对象找到相应虚函数表的指针; 2)根据虚函数表指针,找到虚函数的地址; 3)根据虚函数地址,指向虚函数代码。 由此可见,对虚函数的调用,只有运行阶段才能够确定,故谓之后期绑定或运行时绑定。 - 动态绑定对程序的性能会造成不利影响。如果不需要实现多态就不要使用虚函数。
例子:
#include <iostream>
using namespace std;
class A {
public:
virtual void foo (void) {
cout << "A::foo()" << endl;
}
virtual void bar (void) {
cout << "A::bar()" << endl;
}
};
class B : public A {
public:
void foo (void) {
cout << "B::foo()" << endl;
}
};
int main (void) {
A a;
void (**vft) (void) = *(void (***) (void))&a;
cout << (void*)vft[0] << ' '
<< (void*)vft[1] << endl;
vft[0] ();
vft[1] ();
B b;
vft = *(void (***) (void))&b;
cout << (void*)vft[0] << ' '
<< (void*)vft[1] << endl;
vft[0] ();
vft[1] ();
return 0;
}
二十四、运行时类型信息(RTTI)
1.typeid操作符
A a; typeid (a)返回typeinfo类型的对象的常引用。 typeinfo::name() - 以字符串的形式返回类型名称。 typeinfo::operator==() -类型一致 typeinfo::operator!=() -类型不一致 #include
例子:
#include <iostream>
#include <typeinfo>
#include <cstring>
using namespace std;
class A {
public:
virtual void foo (void) {}
};
class B : public A {};
void print (A* pa) {
if (typeid (*pa) == typeid (A))
cout << "pa指向A对象!" << endl;
else
if (typeid (*pa) == typeid (B))
cout << "pa指向B对象!" << endl;
}
int main (void) {
cout << typeid (int).name () << endl;
cout << typeid (unsigned int).name () << endl;
cout << typeid (double[10]).name () << endl;
cout << typeid (char[3][4][5]).name () << endl;
char* (*p[5]) (int*, short*);
cout << typeid (p).name () << endl;
cout << typeid (const char* const* const).name (
) << endl;
cout << typeid (A).name () << endl;
A* pa = new B;
cout << typeid (*pa).name () << endl;
print (new A);
print (new B);
}
2.dynamic_cast 例子:
#include <iostream>
using namespace std;
class A { virtual void foo (void) {} };
class B : public A {};
class C : public B {};
class D {};
int main (void) {
B b;
A* pa = &b;
cout << pa << endl;
cout << "-------- dc --------" << endl;
B* pb = dynamic_cast<B*> (pa);
cout << pb << endl;
C* pc = dynamic_cast<C*> (pa);
cout << pc << endl;
A& ra = b;
try {
C& rc = dynamic_cast<C&> (ra);
}
catch (exception& ex) {
cout << "类型转换失败:" << ex.what ()
<< endl;
}
D* pd = dynamic_cast<D*> (pa);
cout << pd << endl;
cout << "-------- sc --------" << endl;
pb = static_cast<B*> (pa);
cout << pb << endl;
pc = static_cast<C*> (pa);
cout << pc << endl;
cout << "-------- rc --------" << endl;
pb = reinterpret_cast<B*> (pa);
cout << pb << endl;
pc = reinterpret_cast<C*> (pa);
cout << pc << endl;
pd = reinterpret_cast<D*> (pa); cout << pd << endl; return 0; }
|