c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞

C++学习大纲


**知识无底,学海无涯,我将我学习c++学到的知识,学习这部分前,建议有一部分c
语言的基础,这样学起来更容易掌握如下向大家分享,学识低浅,如有错误,
请君提出,方便我修改。**

文章目录

    • C++学习大纲
  • 一、前言
    • 一、C++ 基本数据类型和表达式
    • 二、C++ 子程序间的数据传递
    • 三、C++ 函数的返回值
    • 四、C++ 标识符的作用域和名字空间
    • 五、C++ 宏与内联函数
    • 六、C++ 函数重载
    • 七、C++ 条件编译
    • 八、C++ 枚举类型
    • 九、C++ 数组类型
    • 十、C++ 结构类型
    • 十一、C++联合类型
    • 十二、C++ 指针类型
    • 十三、C++ 引用类型
    • 十四、C++ 成员的访问控制
    • 十五、C++ 构造和析构函数
    • 十六、C++ this指针
    • 十七、C++ 拷贝构造函数
    • 十八、C++ 常成员函数
    • 十九、C++ 静态成员和静态函数
    • 二十、C++ 继承
    • 二十一、C++多态
    • 二十三、C++ 函数覆盖的条件
    • 二十二、C++ 纯虚函数
    • 二十三、C++ 动态绑定(后期绑定、运行时绑定)
    • 二十四、运行时类型信息(RTTI)


一、前言

  • 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++语言的使用领域:

  1. 游戏开发:强建模能力,性能高。
  2. 科学计算:FORTRAN,C++算法库。
  3. 网络和分布式:ACE框架。
  4. 桌面应用:VC/MFC,Office,QQ,多媒体
  5. 操作系统和设备驱动:优化编译器的发明使C++在底层开发方面可
    以和C向媲美。
  6. 移动终端
    既需要性能,同时又要有面向对象的建模。

语言特点

  1. 支持数据封装和数据隐藏:在C++中,类是支持数据封装的工具,对象则是数据封装的实现。C++通过建立用户定义类支持数据封装和数据隐藏。在面向对象的程序设计中,将数据和对该数据进行合法操作的函数封装在一起作为一个类的定义。对象被说明为具有一个给定类的变量。每个给定类的对象包含这个类所规定的若干私有成员、公有成员及保护成员。完好定义的类一旦建立,就可看成完全封装的实体,可以作为一个整体单元使用。类的实际内部工作隐藏起来,使用完好定义的类的用户不需要知道类是如何工作的,只要知道如何使用它即可。
  2. 支持继承和重用:在C++现有类的基础上可以声明新类型,这就是继承和重用的思想。通过继承和重用可以更有效地组织程序结构,明确类间关系,并且充分利用已有的类来完成更复杂、深入的开发。新定义的类为子类,成为派生类。它可以从父类那里继承所有非私有的属性和方法,作为自己的成员。
  3. 支持多态性:采用多态性为每个类指定表现行为。多态性形成由父类和它们的子类组成的一个树型结构。在这个树中的每个子类可以接收一个或多个具有相同名字的消息。当一个消息被这个树中一个类的一个对象接收时,这个对象动态地决定给予子类对象的消息的某种用法。多态性的这一特性允许使用高级抽象。
    c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞_第1张图片

学习c++之前,首先要了解,
什么是对象?

  1. 万物皆对象
  2. 程序就是一组对象,对象之间通过消息交换信息
  3. 类就是对对象的描述和抽象,对象就是类的具体化和实例化

通过类描述对象

  1. 属性:姓名、年龄、学号
  2. 行为:吃饭、睡觉、学习
  3. 类就是从属性和行为两个方面对对象进行抽象。

面向对象程序设计(OOP)
现实世界 虚拟世界
对象 -> 抽象 -> 类 -> 对象
1.至少掌握一种OOP编程语言
2.精通一种面向对象的元语言—UML
3.研究设计模式,GOF
有了这一部分的知识,让我们开始踏上c++的学习之旅吧!
温馨提示

  • 本文里的代码都建议学习者敲一敲,可能文章写的并不是太好,并且借用了许多他人的知识,但是文章内的代码都值得学习者敲一敲,唯有多上机操作,才能更好地掌握这本编程语言的核心!

一、C++ 基本数据类型和表达式

1.C++是一种静态类型语言(运行前指定每个数据的类型),也是一种强类型语言(对数据的操作进行严格的类型检查)。

  1. bool类型数据在算术运算时true对应1,false对应0。

【注意】关系运算符结合型均为左结合右,整体优先级低于算术运算符,高于赋值运算符
在C++中用数值1代表“真”,用0代表“假”

  1. typedef给已有类型取别名
    typedef <已有类型> <别名>;

  2. 表达式中的类型转换
    static_cast <类型说明符> (表达式)
    用于一般表达式的类型转换
    reinterpret_cast <类型说明符> (表达式)
    用于非标准的指针数据类型转换
    const_cast <类型说明符>( 表达式)
    将const表达式转换成非常量类型,常用于将限制const成员函数const定义解除
    dynamic_cast <类型说明符>(表达式)
    用于进行对象指针的类型转换

  3. 表达式中的类型转换
    static_cast <类型说明符> (表达式)
    用于一般表达式的类型转换
    reinterpret_cast <类型说明符> (表达式)
    用于非标准的指针数据类型转换
    const_cast <类型说明符>( 表达式)
    将const表达式转换成非常量类型,常用于将限制const成员函数const定义解除
    dynamic_cast <类型说明符>(表达式)
    用于进行对象指针的类型转换

  4. 常量包括两种:字面常量和符号常量。

    字面常量:直接写出来的

    符号常量:又称命名常量,指有名字的常量,如 const double PI=3.1415; #define PI 3.1415

    在定义变量时,如果在前面加上关键字const,则变量的值在程序运行期间不能被改变,
    格式
    const 类型说明符 变量名
    注意:
    在定义常变量时必须同时对变量进行初始化,此后它的值不能再改变,即不能给它赋值
    在C标准中,const 定义的常量是全局的,但C++中视声明位置而定
    #define 和const的区别:
    const常量有数据类型,而宏变量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只能进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误,
    有些集成化的调试工具可以对const常量进行调试,但不能对宏变量进行调试。

  5. 符号常量

    用一个符号名来代替一个常量,称为符号常量。
    使用预处理命令#define 指定的
    #define 格式(宏定义命令)
    简单的宏定义 #define <宏名> <字符串>
    带参数的宏定义 #define <宏名> (<参数表>) <宏体>
    符号常量是个名字,但它不是变量,在它的作用域内其值不能改变,也不能赋值。

    符号常量的作用

    1)增加程序易读性

    2)提高程序对常量使用的一致性

    3)增强了程序的易维护性

  6. 自增自减运算符
    前缀运算是先变化后运算,后缀运算是先运算后变化。

  7. 变量定义要给变量分配内存空间,而声明没有;定义可以初始化,声明不能。

    声明: extern <类型名> <变量名>;

            头文件中使用extern语句对一个文件中的全局函数进行声明;
     
            函数使用一个全局变量但是还未见到其定义时使用extern语
            句对其进行声明。
    
  8. 逻辑运算符与表达式
    优先级(高到低)
    !(逻辑非)
    &&(逻辑与)
    ||(逻辑或)
    对于逻辑与运算,如果第一个操作数被判断为假, 系统不再判断或求解第二操作数
    对于逻辑非运算,如果第一个操作数被判断为真,系统不再判断或求解第二操作数

  9. 当运算结果已经确定时,后面的表达式就不会再执行。

  10. 类型转换

    隐式类型转换 -> 显示类型转换

    int i=-10; unsigned int j=1; i < j 的值是false,而不是true

  11. 操作符的优先级

    1)按单目、双目、三目、赋值依次降低

    2)按算术、移位、关系、逻辑位、逻辑依次降低

  12. 表达式中操作数的类型转换

    逐个操作符进行类型转换

如: short int a; int b; double c;

    a*b/c; 先a > int, 然后(a*b) > double
  1. 位运算符与表达式
    & | ^ ~ << >>
    按位与(&)
    按位与运算的作用是将两个操作数对应的每一位分别进行逻辑与运算
    按位或(|)
    按位或运算的作用是将两个操作数对应的每一位分别进行逻辑或运算
    按位异或(^)
    按位异或操作的作用是将两个操作数对应的每一位进行异或。
    运算规则
    若对应位相同,则该位的运算结果为0;若对应位不同,则该位的运算结果为1
    按位取反(~)
    按位取反是一个单目运算符,其作用是对一个二进制数的每一位取反
    左移位(<<)
    按照指定的位数将一个数的二进制值向左移位,左移后,低位(右边空位)补0,移出的高位(左边)舍弃
    右移位(>>)
    按照指定的位数将一个数的二进制值向右移位,右移后移出的低位舍弃,如果是无符号数则高位补零,如果是有符号数,则高位补符号位

  2. SIZEOF运算符
    返回指定的数据类型或表达式值的数据类型在内存中占用的字节数
    两种形式
    sizeof(类型说明符)
    sizeof(char)
    sizeof(表达式)
    sizeof(66)

  3. 逗号运算符
    逗号运算符的优先级别最低,结合方向自左至右,其功能是把两个表达式连接起来组成一个表达式,称为逗号表达式
    格式:
    表达式1,表达式2,表达式3,……,表达式n
    注意
    计算一个逗号表达式的值时,从左至右依次计算各个表达式的值,最后计算的一个表达式的值和数据类型便是整个逗号表达式的值和类型
    x=25,x4;
    逗号表达式可以嵌套

  4. 敲重点:

    计算过程中要注意数据的底层表示(是否溢出等)、表达式的副作用(短路求值等)。


二、C++ 子程序间的数据传递

  1. 通过全局变量

    全局变量可以定义在函数外的任何地方,但是如果在使用一个全局变量时未见到它的定义,就要使用extern语句对其进行声明。

  2. 通过子程序的参数和返回值机制

    1)值传递:传递实参的一个拷贝,可以阻止子程序通过形参改变实参,但最多只能返回一个值

    2)地址/引用传递:传递实参的地址,可以提高参数传递的效率,可以返回多个执行结果,但是会降低数据访问效率(通过间接的方式访问传输的数据)、可通过形参改变实参


三、C++ 函数的返回值

  • 首先,强调一点,和函数传参一样,函数返回时也会做一个拷贝。从某种角度上看,和传参一样,也分为三种:
  1. 返回值:返回任意类型的数据类型,会将返回数据做一个拷贝(副本)赋值给变量;由于需要拷贝,所以对于复杂对象这种方式效率比较低(调用对象的拷贝构造函数、析构函数);例如:int test(){}或者 Point test(){}
  2. 返回指针:返回一个指针,也叫指针类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int *test(){} 或者 Point *test(){}
    返回引用:返回一个引用,也叫引用类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构
  3. 函数和析构函数;例如: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++ 标识符的作用域和名字空间

包括:局部作用域、全局作用域、文件作用域、函数作用域、名空间作用域、类作用域

注意:潜在作用域,也就是同名变量的作用域问题。
识符的作用域:

  1. 局部作用域

    指在函数定义或者复合语句中,从标识符的定义点开始到函数或者复合语句结束之间的程序段。

    在同一个局部作用域内不能出现相同名字的两个局部变量(包括形参)。

    一个函数内的复合语句又是一个局部作用域,也就是在函数内有某个变量时,复合语句中可以有另外一个同名字的变量。

  2. 全局作用域

    指对构成C++程序的所有源文件。

    在C++标准中,把全局作用域归于连接控制范畴。

    通常把全局标识符的生命放在某个头文件中。

标识符的作用域是其最进的一对大括号。

#include 
using namespace std;

int i;			//在全局命名空间中的全局变量

namespace Ns {
	int j;		//在Ns命名空间中的全局变量
}

int main() {
	i = 5;			//为全局变量i赋值
	Ns::j = 6;		//为全局变量j赋值
	{				//子块1
		using namespace Ns; //使得在当前块中可以直接引用Ns命名空间的标识符
		int i;		//局部变量,局部作用域
		i = 7;
		cout << "i = " << i << endl;//输出7
		cout << "j = " << j << endl;//输出6
	}
	cout << "i = " << i << endl;	//输出5
	return 0;
}
  1. 对象的生存期:
    static声明的局部变量生存期是整个程序
    static语句只有最开始使用一次,a,b的值存储在内存中,第二次调用other使用上次other结束时的a,b值(a=4,b=4)。
#include 
using namespace std;
int i = 1;	// i 为全局变量,具有静态生存期

void other() {
	//a, b为静态局部变量,具有全局寿命,局部可见,只第一次进入函数时被初始化
	static int a = 2;
	static int b;
	//c为局部变量,具有动态生存期,每次进入函数时都初始化
	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() {
	//a为静态局部变量,具有全局寿命,局部可见
	static int a;
	//b, c为局部变量,具有动态生存期
	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;
}
  1. 文件作用域

    指单独的一个源文件。

    在全局标识符的定义中加上static修饰符,该全局标识符就变成了具有文件作用域的标识符。
    量在本文件中定义。

量在本文件中定义直接访问。。
// file_1.cpp
int counter;   // definition
// file_1.cpp
cout << counter++ << endl;  
变量在其它文件中定义。
在访问前先要声明,然后才可以使用
// file_1.cpp
int counter;   // definition
// file_2.cpp
extern int counter; // declaration
cout << couter++ << endl; // counter是在文件file_1.cpp中定义的全局变量
  • 声明没有定义变量,只是告诉编译器在其它文件中有counter这个变量。所以在file_2.cpp中,counter是file_1.cpp定义的全局变量。
    注意:我们要区分其它文件与Include。如果include了这个文件,就不再是其它文件了。比如下面的例子,这个时候counter对file_2.cpp是可见了。因为file_2.cpp已经包含了文件file_1.cpp。
// file_2.cpp
#include file_1.cpp
  1. 名空间作用域
#include "Fasdsad.h"
#include 
Fasdsad::Fasdsad()
{
}
 
Fasdsad::~Fasdsad()
{
}
using namespace std;
int i;    
int k = 100;
 
namespace Mode {                //声明模块
	int j;                      //Mode命名空间的全局变量;
}
int main() {
	i = 5;
	Mode::j = 4;
	{
		using namespace Mode;
		int i = 2;
		j = 10;
		int f = 20;
		cout << "i=" << i << endl;             //显示i=2,说明具有局部作用域的变量可以把具有全局作用域的变量隐藏
		cout << "j=" << j << endl;
		cout << "k=" << k << endl;             //显示k=100,全局命名空间中的变量在Mode命名空间中可以调用。
	}
	cout << "i=" << i << endl;
//	cout << "f=" << f << endl;                 //加上这句报错,在Mode命名空间中的申请的变量在全局命名空间中不能够访问。
	system("pause");
	return 0;
}
  • 名字空间

  • 对程序中的标识符(类型、函数、变量),
    按照某种逻辑规则划分成若干组。

  • 定义名字空间
    namespace 名字空间名 {
    名字空间成员;
    }

  • 使用名字空间
    1.作用于限定符:名字空间名::名字空间成员,
    表示访问特定名字空间中的特定成员。
    例子:

#include 
int main (void) {
	std::cout << "Hello, World !" << std::endl;
	int i;
	double d;
	char s[256];
   //	scanf ("%d%lf%s", &i, &d, s);
	std::cin >> i >> d >> s;
   //	printf ("%d %lf %s\n", i, d, s);
	std::cout << i << ' ' << d << ' ' << s << '\n';
	return 0;
}
  1. 名字空间指令:
    using namespace 名字空间名;
    在该条指令之后的代码对指令所指名字空间中的所有成员都可见,
    可直接访问这些成员,无需加“::”。
    例:using namespace std;
  2. 名字空间声明:
    using 名字空间名::名字空间成员;
    将指定名字空间中的某个成员引入当前作用域,
    可直接访问这些成员,无需加“::”。
  3. 匿名名字空间
    如果一个标识符没有被显示地定义在任何名字空间中,
    编译器会将其缺省地置于匿名名字空间中。
    对匿名名字空间中的成员通过“::名字空间成员”的形式访问。
  4. 名字空间合并
  5. 名字空间嵌套
namespace ns1 {
  namespace ns2 {
    namespace ns3 {
      void foo (void) { ... }
    }
  }
}
ns1::ns2::ns3::foo ();
using namespace ns1::ns2::ns3;
foo ();

例子:名字空间

#include 
using namespace std;
//namespace {
	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; // 名字空间声明
//	using abc::salary;
	salary (6000);
	cout << "建行:" << ccb::balance << endl;
	abc::print (abc::balance);
	return 0;
}

五、C++ 宏与内联函数

因为函数调用需要开销(如:保护调用者的运行环境、参数传递、执行调用指令等),所以函数调用会带来程序执行效率的下降,特别是对一些小函数的频繁调用将是程序的效率有很大的降低。

C++提出了两种解决方法:宏、内联函数。

  1. 宏定义是一种简单的语义替换。
    它在元编程时有两种风格
//第一种,这里identifier是宏的名字,replacement-list是零个或多个标记序列,
//后继的程序文本中所有出现的identifier的地方都会被预处理器展开为相应的replacemen-list
#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

  1. 内联函数
    内联函数是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); // a = 5.0 * 5.0
b = SQUARE(4.3 + 3.1); // 4.3 + 3.1 * 4.3 + 3.1
c = SQUARE(d++) // c == d++ * d++ 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

  • 函数重载的作用:
    重载函数通常用来在同一个作用域内 用同一个函数名 命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

  • 函数重载是一种静态多态:
    (1)多态:用同一个东西表示不同的形态;
    (2)多态分为:
    静态多态(编译时的多态);
    动态多态(运行时的多态);
    (3)函数重载是一种静态多态;
    那就让我们来看看函数重载的例子吧 ↓

#include  //操作符重载
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:返回右值,返回的对象不能接受赋值。
	// 第二个const:支持常量型右操作数,可以接受有常属性的变量为参数,将引用声明为常引用才能指向常变量,常引用也可以指向非常变量。
	// 第三个const:支持常量型左操作数,使this指针变为常指针,也可以指向有常属性的变量。
	const Complex operator+ (const Complex& r) const { //操作符重载的成员函数形式:L.operator+(R)
		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) {           //操作符重载的全局函数形式,::operator-(L,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 = c1.operator+ (c2)
	c3.print (); // 4+6i
//	(c1 + c2) = c3;
	c3 = c2 - c1; // c3 = ::operator- (c2, c1)
	c3.print (); // 2+2i
	return 0;
}
// +=/-=/*=...
// 左变右不变。
// 表达式的值是左值,左操作数的引用。
// (a += b) = c;
#include 
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) {  //返回的是左操作数的引用,是可以赋值的,所以最前面不加const,调用该函数的左操作数是要改变的也就是this会修改,也就是调用函数的参数被修改,所以{前也不加const。 
		m_r += r.m_r;
		m_i += r.m_i;
		return *this;//返回该调用操作数
	}
        //全局函数形式:(把定义与声明合二为一,因为有friend,所以是全局函数)
	friend Complex& operator-= (Complex& l,
		const Complex& r) {            //第一个参数l为左操作数引用,可被修改,所以不加const,而第二个参数r为右操作数,值不会被修改,所以加const。
		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.operator+= (c2)
	c1.print (); // 4+6i
	Complex c3 (5, 6);
	(c1 += c2) = c3;//返回的是左操作数c1的引用,把c3赋值给c1.
	c1.print (); // 5+6i
	c1 -= c2; // ::operator-= (c1, c2)
	c1.print (); // 2+2i
	(c1 -= c2) = c3;
	c1.print (); // 5+6i;
	return 0;
}
//<>
//int i = 10;
//float f = 1.23;
//Complex c (...);
//cout << c << i << f << endl;
//cin >> c;
//左操作数ostream/istream类型,不能是常量,不能拷贝。
//右操作数自定义类型,对于<<可以是常量,对于>>不能是常量。
//表达式的值是左操作数的引用。
//::operator<< (cout, c).operator<< (i).operator<< (f).operator<< //(endl);
#include 
//输入与输出。
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) {               //因为右操作数是要输出的,所以右操作数可以是常量,所以声明为常引用,加const,也可做非常变量的引用。
		return os << r.m_r << '+' << r.m_i << 'i';//返回左操作数的引用
	}
	
        friend istream& operator>> (istream& is,//返回的是左操作数的引用
		Complex& r) {                   //因为右操作数是要输入的,所以右操作数不能是常量,不加const
		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;
//	::operator<<(::operator<<(cout,c1).operator<<(
//		endl),c2).operator<<(endl);
	cin >> c1 >> c2;
	cout << c1 << endl << c2 << endl;  
	return 0;
}

缺省参数和哑元参数

  1. 如果调用一个函数时,没有提供实参,那么对应形参就取缺省值。
  2. 如果一个参数带有缺省值,那么它后边的所有参数必须都带有缺省值。
  3. 如果一个函数声明和定义分开,那么缺省参数只能放在声明中。
  4. 避免和重载发生歧义。
  5. 只有类型而没有名字的形参,谓之哑元。
    i++ - operator++
    ++i
    V1: void decode (int arg) { … }
    V2: void decode (int) { … }

例子1:重载与缺省值

#include 
using namespace std;
void foo (int a = 10, double b = 0.01, 
	       const char* c = "tarena");   //函数1
void foo (void) {}                      //函数2
//函数1与函数2构成重载关系
void bar (int) {                        //函数3
	cout << "bar(int)" << endl;
}
void bar (int, double) {                //函数4
	cout << "bar(int,double)" << endl;
}
//函数3与函数4构成重载关系
int main (void) {
	foo (1, 3.14, "hello");//调用函数1
	foo (1, 3.14);         //调用函数1

	foo (1);               //调用函数1 
//	foo (); // 歧义 ,可以调用函数2,但也可以调用函数1,因为函数1在不提供实参的情况下,可以取缺省值。
	bar (100);             //调用函数3
	bar (100, 12.34);      //调用函数4
	return 0;
}

例子2:重载与作用域

#include 
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;    //10 调用函数1,作用域可见ns2与ns1,所以与函数2构成重载
	cout << foo (1.23) << endl;  //1.23 调用函数2,作用域可见ns2与ns1,所以与函数1构成重载



	using ns1::foo;              //名字空间声明
(当同时出现名字指令与名字空间声明,则名字空间声明会隐藏名字空间指令)
	cout << foo (10) << endl;    //10,调用函数1,只可见名字空间ns1的foo(),所以也并不构成重载。
	
   cout << foo (1.23) << endl;  //10,调用函数1,只可见名字空间ns1的foo(),所以也并不构成重载。

	using ns2::foo;              //名字空间声明
	cout << foo (10) << endl;    //10,调用函数1,可见名字空间ns1与名字空间ns2的foo(),所以构成重载。
	
  cout << foo (1.23) << endl;  //1.23,调用函数2,可见名字空间ns1与名字空间ns2的foo(),所以构成重载。
	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
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++语言提供的一个预定义的枚举类型。
  1. 枚举类型定义

    enum <枚举类型名> {<枚举值表>};

  2. 初始化

    枚举类型的每一个枚举值都对应一个整型数,默认情况下,第一个枚举值的值是0,然后依次增1,但也可以显示初始化任意一个枚举值对应的整形数,没定义的枚举值默认情况下在其前一个枚举值的对应整型数上加1.

    留个问题:如果多个枚举值对应同一个整形数会怎样?

    enum Day {Sun=7, MON=1, TUE, WED, THU, FRI, SAT}

  3. 枚举变量的定义

    <枚举类型> <变量表>;

或<枚举类型>{<枚举值表>} <变量表>;

  1. 枚举变量的使用

    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. 一维数组

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. 二维数组

    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}};//第一个下标可以省略,其他的不能,更高维的数组也同此。
    
     按行存储!
    
  2. 三维数组

    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. 字符数组

    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;   //创建这种类型的变量。  //可以省略关键字 struct。
//能够省略,说明结构声明 定义了一种新类型。

hat.volume;  //使用成员运算符(.)访问各个成员。
//structur.cpp
#include 
using namespace std;
struct inflatable   //结构位置很重要 //通常将结构使用外部声明。
{                   //可供全部函数使用
	char name[20];
	float volume;
	double price;
}

int main()
{
	inflatable guest =    //结构初始化
	{     // 与数组一样,C++11也支持结构的 列表初始化。,并且=可选。
		"Glorous Gloria",   //可以使用,进行分隔开。
		1.88,
		29.99
	}
	//inflatable guest{"Glorous Gloria", 1.88, 29.99}
	cout<< "Expand your guest list with "<<guest.name;

	return 0;
}

结构类型用于表示由固定多个、类型可以不同的元素所构成的复合数据类型。

  1. 结构类型定义

    struct <结构类型名> {<成员表>};

或 typedef struct <结构类型名> {<成员表>}<结构体类型别名>;

1)别名可以跟结构类型名不一样,但是一般都是一样的,设置别名是为了方便像其他变量类型一样定义变量,这是保留了C的语法。

2)在结构类型定义时,对成员变量进行初始化是没有意义的,因为类型不是程序运行时刻的实体,它们不占用内存空间。
  1. 结构类型变量定义

    struct <结构类型名> <变量名表>;//C的用法

或 <结构类型名> <变量名表>;// C++的用法

或 struct <结构类型名> {<成员表>}<变量名表>;

  1. 操作

    1)访问成员:<结构类型的变量名>.<成员名>

    2)对结构类型的数据可以进行整体赋值,但是要保证两者属于相同的结构(成员名和类型都相同)。

  2. 存储

    结构类型的变量在内存中占用一块连续的存储空间。

  3. 结构类型的默认参数传递方式是值传递,因此,当结构类型很大时传输速度回受限。

  4. 定义完结构类型后,其使用和平时的类型没有太大的区别,该加加该减减,不过要记住其每个成员也是一个实体。

#if 0
/*结构体指针*/
#include
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)大小能被其包含的所有基本数据类型的大小所整除。

  1. 联合类型的定义

    union <联合类型名> {<成员表>};

    与结构类型类似,只是把struct 换成了 union.

    在语义上,联合类型和结构类型的区别是,联合类型的所有成员占用同一块内存空间,该内存的空间大小是其最大成员的内存空间大小。

  2. 操作

#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 
 
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个字节。可以看到联合体中的元素都是占用的同一块存储区域,起始地址都是同一个地址
更多操作参考这篇博客

  1. 联合类型除了可以实现用一种类型表示多种类型的数据外,还可以实现多个数据共享内存空间,从而节省了内存空间。所以,当一些大型的数组变量,当它们的使用分布在程序的各个阶段时(不是同时使用),就可以使用联系类型来描述。

十二、C++ 指针类型

指针,用来描述内存地址,并通过提供指针操作来实现与内存相关的程序功能。

  1. 定义

    <类型>* <指针变量>;

    类型决定了指向的内存空间的大小。

    指针变量也是一种变量,有着自己的内存空间,该空间上存储的是另一个变量的内存空间。

    可以使用typedef取别名来减少定义变量时的一些麻烦,如typedef int* Pointer;

  2. 操作

    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<
  1. 指向常量的指针变量

const <类型> *<指针变量>;
含义:不能改变指向的地址上的值(无论该地址上是常量还是变量),但是该变量的值是可以改变的。

  1. 指针与动态变量

    动态变量是在程序运行时才产生,并在程序结束前消亡。动态变量跟局部变量不同,在程序运行前编译程序就知道了局部变量的存在

    创建:

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)
应用:动态数组、链表
  1. 指针 VS 无符号整数

    指针从形式上看属于无符号数,但是指针可以关联到程序实体(变量或函数),指针指向某个内存地址,无符号整数的某些运算不能实施在指针上(如乘法和除法就不能)。

  2. new VS malloc

    1)new 自动计算所需分配的空间大小,而malloc需要显示指出。

    2)new自动返回相应类型的指针,而malloc要做强制类型转换。

    3)new会调用相应对象类的构造函数,而malloc不会。

    对于new 和 malloc,如果程序的堆区没有足够的空间可供分配,则产生bad_alloc异常(会返回空指针NULL)。

  3. delete VS free

    1)delete会调用析构函数,free不会。

    2)delete或free一个指针时,其实只是从编译器释放了这个地址的内存,但指针仍然指向该地址,此时的指针叫做悬浮指针,悬浮指针不为空指针,依据可以用来赋值或者和使用,所以会产生语义错误。(怎么解决呢?在delete或free后将指针设置为NULL)。

    3)如果没有进行delete或free操作,就将指针指向别处,之前分配的内存空间就会一直存在但不能再被使用,也就是说造成了内存泄漏。

  4. 函数指针

    函数指针就是指向函数的指针。

    定义格式:<返回类型> (*<指针变量>)(<形式参数表>); 如: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)){};
    
  5. 指针与数组

    对数组元素的访问通常是通过下标来实现的,但是频繁地采用这种方式,有时效率不高,因为每次访问都要先通过下标计算数组元素的地址(就是上面的提到的指针变量加上一个整型数)。

  6. 多级指针

    指针除了可以指向一般类型的变量外,还可以指向指针类型的变量。指针变量要初始化后才能使用。

    如果一个指针变量没有初始化或者赋值,访问它所指向的变量将会导致运行时刻的严重错误。

int x;

int *p;

int **q;

*p=1; //Error, p未初始化,p指向的空间不知道是什么

*q=&x; //Error,q未初始化

q=&p; //OK

**q=2; //Error, q指向的变量p未初始化
  1. 指向常量的指针类型 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++ 引用类型

  1. 什么叫引用,通俗点来说,就是一个人的外号或者叫做小名
  2. 我们为什么要使用引用?
    使用引用类型就不必在函数中声明形参是指针变量。指针变量要另外开辟内存单元,其内容是地址。而引用变量不是一个独立的变量,不单独占内存单元,达到节省内存空间的作用。
  • 定义:
    引用:就是变量的一个别名。
    <类型> &<引用变量>;
int a = 20;
int& b = a; // int* b = &a;
b = 10; // *b = 10;
cout << a << endl; // 10

定义时必须要初始化

int a;
int* p;
a = 20;
p = &a;
int& b; // ERROR !
int& b = a; // OK
如: int x=0; 
int &y=x;
y=2;//此时x就是y的别名,因为y=2,所以x=2
  1. 引用一旦初始化就不能再引用其它变量。
int a = 20, c = 30;
int& b = a;
b = c; // c => b/a
  1. 引用的应用场景
    1)引用型参数
    a.修改实参
    b.避免拷贝,通过加const可以防止在函数中意外地修改实参的值,同时还可以接受拥有常属性的实参。
    2)引用型返回值
int b = 10;
int a = func (b);
func (b) = a;

从一个函数中返回引用往往是为了将该函数的返回值作为左值使用。但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定的后果。
不要返回局部变量的引用,可以返回全局、静态、成员变量的引用,也可以返回引用型形参变量本身。

  1. 结构体类型的引用
#include 
using namespace std;
typedef struct Coor
{
	int x;
	int y;
};
int  main(){
	Coor c1;
	Coor &c2 = c1; //此处使用了引用,c2也就是c1的引用,也就是说c2是c1的别名
	c2.x = 10;
	c2.y = 20;
	cout<<c1.x<<" "<<c1.y<<endl;
	system("pause");
	return 0;
}
  1. 指针类型的引用
    类型 *&指针引用名 = 指针;
#include 
using namespace std;
int  main(){
	int a = 10;
	int *p = &a;//同理 类型 *&指针引用名 = 指针;
	int *&q = p;
	*q= 20;
	cout<<a<<endl;
	system("pause");
	return 0;
}
  1. 引用作函数参数
//将两个值进行交换
void fun( int *a,int *b) //形参为两个整型的指针变量
{
    int temp = 0;	//定义一个临时变量。良好的习惯是定义一个变量并初始化它;
    c = *a;	//将*a赋值给c;
    *a = *b;	//将*b赋值给*a;
    *b = c;	//再将c赋值给*b;这样就完成了a、b数值的交换
}
 
	int x = 10,y = 20;
	fun(&x,&y); //在主函数中调用时,传过去的实参需要写成 取地址a,取地址b,比较麻烦,也不易理解。

使用引用参数可以直接操作实参变量,从而能够实现通过修改形参的值而达到修改对应实参值得目的。当引用作为函数形参,其引用的目标变量没人为调用该函数时对应的实参变量名,所以,在定义函数时,对于引用类型参数不必提供引用的初始值。


十四、C++ 成员的访问控制

首先解释几个特定词,下面要用到:

水平权限:在一个类中,成员的权限控制,就是类中的成员函数能否访问其他成员、类的对象能否访问类中某成员。
垂直权限:在派生类中,对从基类继承来的成员的访问。
内部访问:类中成员函数对其他成员的访问。
外部访问:通过类的对象,访问类的成员函数或者成员变量,有的书里也称之为对象访问。
C++的水平权限控制
当private,public,protected单纯的作为一个类中的成员(变量和函数)权限设置时:
类的成员函数以及友元函数可以访问类中所有成员,但是在类外通过类的对象,就只能访问该类的共有成员。
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数;这里将友元函数看成内部函数,方便记忆!

总结为下表:
c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞_第2张图片

程序验证如下:这里没有friend的成员,另外成员变量和成员函数的权限控制是一样的。

#include  

class Foo
{
public:
    int a;
    void public_fun();
protected:
    char b;
    void protected_fun();
private:
    bool c;
    void private_fun();
};

//验证public成员内部可见性
void Foo::public_fun()
{
    a = 10;
    b = 'c';
    c = true;
}

//验证protected成员函数的内部可见性
void Foo::protected_fun()
{
    a = 10;
    b = 'c';
    c = true;
}

//验证private成员函数的内部可见性
void Foo::private_fun()
{
    a = 10;
    b = 'c';
    c = true;
}

int main()  
{  
    Foo foo;
    foo.public_fun();//验证public成员外部可见性
    foo.protected_fun();//验证protected成员外部可见性,这里提示错误
    foo.private_fun();//验证private成员外部可见性,这里提示错误
    return 0;  
}

C++的垂直访问控制
当private,public,protected作为继承方式时:
派生类可以继承基类中除了构造函数与析构函数(凡是与具体对象的类别息息相关的都不能被继承,赋值运算符重载函数也不能被继承)之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性是由继承方式控制的。
总结为下表:
c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞_第3张图片
派生类对基类成员的访问形式主要有以下两种:

内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。

本章节参考[博客](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++ 构造和析构函数

学习这部分前,我们再重新温习一下类的基本语法。

  1. 类的定义
    class 类名 {
    };

    class Student {
    };
  2. 成员变量——属性
    class 类名 {
    类型 成员变量名;
    };

    class Student {
    string m_name;
    int m_age;
    };
  3. 成员函数——行为
    class 类名 {
    返回类型 成员函数名 (形参表) {
    函数体;
    }
    };
class Student {
  string m_name;
  int    m_age;
  void eat (const string& food) {
    ...
  }
};

4.访问控制属性
1)公有成员:public,谁都可以访问。
2)私有成员:private,只有自己可以访问。
3)保护成员:protected,只有自己和自己的子类可以访问。
4)类的成员缺省访控属性为私有,而结构的成员缺省访控属性为公有。
例子:

#include 
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");    //你才2
   student.setAge (-100);    //无效年龄
   student.setName ("张飞"); //将其赋值给成员变量m_name
   student.setAge (20);      //将其赋值给成员变量m_age
	student.eat ("包子");     //20岁的张飞同学正在吃包子
	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()
{
    //1.我们按照C++编译器提供的初始化对象和显示的初始化对象
    Test a(10);
    Test b;      //显示创建对象但是还是调用无参构造函数,之后显示调用了初始化函数
    b.init(10);

    //创建对象数组(使用构造函数)
    Test arr[3] = {Test(10),Test(),Test()};
    //创建对象数组 (使用显示的初始化函数)
    Test brr[3];   //创建了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 
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;
}
  • 运行结果:

    李华的年龄是16,成绩是96.5
    
    
    this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。
    本例中成员函数的参数和成员变量重名,只能通过 this 区分。以成员函数setname(char *name)为
    例,它的形参是name,和成员变量name重名,如果写作name = name;这样的语句,就是给形参
    name赋值,而不是给成员变量name赋值。而写作this -> name = name;后,=左边的name就是成
    员变量,右边的name就是形参,一目了然。
    

注意,this 是一个指针,要用->来访问成员变量或成员函数。

说什么?你看不懂?那就给你们看个简易版的

class A
{
   int x,y;
public:
   void f();
   void g(int x){
     this->x = x;//将类内的x的值赋值给了g()函数内部的x
   }
}

这个是不是就显然易懂了呢。

  • this 到底是什么
    this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
    this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
    在《 C++函数编译原理和成员函数的实现》一节中讲到,成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。 这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。

让我们用三个例子,在深刻熟悉this指针的同时,重新复习一下上一章节构造和析构函数的使用方法吧!

例子1:

#include 
using namespace std;
class A {
public:
	A (int data) : data (data) {
		cout << "构造: " << this << endl;
//		this->data = data;
	}
	void foo (void) {
		cout << "foo: " << this << endl;
		cout << this->data << endl;
	}
	int data;
};
int main (void) {
 A a (1000);                //创建对象调用了构造函数,并输出this的地址,输出“构造:0xbf9b24d8”
cout << "main: " << &a << endl;//输出该对象的地址,输出“main:0xbf9b24d8”
a.foo ();                   //该对象调用foo函数,输出this的值,以及输出this->data的值,输出“foo:0xbf9b24d8  1000”
A* pa = new A (1000);       //创建对象调用构造函数,输出this的地址,输出“构造:0x95cc008”
cout << "main: " << pa << endl; //输出该对象的地址,输出“main:0x95cc008”
pa->foo ();                 //该对象调用foo函数,输出this以及this->data的值,输出“foo:0x95cc008  1000”
delete pa;
}

例子2:

#include 
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 ();
//	c.inc ();
//	c.inc ();
	c.inc ().inc ().inc ();//函数返回的是一个别名,是一个左值,可以用来调用函数
	c.print ();            // 输出为3,如果前面的函数不加&,返回的只是拷贝,输出为1。
	return 0;
}

例子3:学生与老师

#include 
using namespace std;
class Student;   //因为在Teacher中会用到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);//将问题question和Teacher类变量的地址作为参数传递给Student类中的ask成员函数,并在ask函数中得到一个值作为参数传递给Teacher类中的replay函数,将值赋给m_answer,最后完成输出。
	cout << "答案:" << m_answer << endl;
}
int main (void) {
	Teacher t;
	Student s;
	t.educate (&s);
	return 0;
}

十七、C++ 拷贝构造函数

  • 定义
    <类名> (const <类名>&);
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 
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) {    //用Inerger类变量时实参给函数中Interger类的形参赋值,同样会调用拷贝构造函数
	i.print ();
}
Integer bar (void) {
	Integer i;
	return i;
}
int main (void) {
	Integer i1 (10);
	i1.print ();//正常创建对象,输出“10”
	Integer i2 (i1); // 调用拷贝构造,输出“拷贝构造”
	i2.print ();  //调用print函数,输出“10”
	Integer i3 = i1; // 调用拷贝构造,输出“拷贝构造”
	i3.print ();  //调用print函数,输出“10”
//	Integer i4 (10, 20);
	cout << "调用foo()函数" << endl;
	foo (i1);     //调用拷贝构造函数,且调用print函数输出,所以输出为“拷贝构造  10”
	cout << "调用bar()函数" << endl;
	Integer i4 (bar ());
	return 0;
}

由[实例],得拷贝构造函数心得

拷贝赋值运算符函数

形如 class X {
  X& operator= (const X& that) {
    ...
  }
};的成员函数称为拷贝赋值运算符函数。
  • 如果一个类没有定义拷贝赋值运算符函数,系统会提供一个缺省拷贝赋值运算符函数。缺省拷贝赋值运算符函数对于基本类型的成员变量,按字节复制,对于类类型的成员变量,调用相应类型的拷贝赋值运算符函数。
  • 在某些情况就下,缺省拷贝赋值运算符函数只能实现浅拷贝,如果需要获得深拷贝的复制效果,就需要自己定义拷贝赋值运算符函数。

例子:拷贝赋值运算符函数

#include 
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 (); // 30
	i3 = i1; // 拷贝赋值
	// i3.operator= (i1);
	i3.print (); // 10
	i3.set (40);
	i3.print (); // 40
	i1.print (); // 10
	/*
	int a = 10, b = 20, c = 30;
	(a = b) = c;
	cout << a << endl;
	*/
	(i3 = i1) = i2;
//	i3.operator=(i1).operator=(i2);
	i3.print ();
	i3 = i3;
	i3.print ();
	return 0;
}

十八、C++ 常成员函数

声明:<类型标志符>函数名(参数表)const;

说明:

(1)const是函数类型的一部分,在实现部分也要带该关键字。

(2)const关键字可以用于对重载函数的区分。

(3)常成员函数不能更新类的成员变量,也不能调用该类中没有用const修饰的成员函数,只能调用常成员函数。

① 首先,常成员函数内部不允许进行数据成员的修改,但是可以在函数内部输出const数据成员与非数据成员!

② 其次,还可以区分同名构造函数,举个例子(如下):

#include 
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
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++中,采用静态成员来解决同一个类的对象共享数据的问题。类的静态成员分为静态数据成员和静态成员函数。

  1. 静态数据成员:
    类中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。
    静态数据成员在一个类中只分配一次存储空间,也就是一个类的所有对象的静态数据成员共享一块存储空间。
    在计数时往往使用的就是静态数据成员。
    【定义】静态数据成员实际上是类域中的全局变量。
静态数据成员的定义(初始化)不应该被放在头文件中。其定义方式
与全局变量相同。如下:

xxx.h文件 
lass A
{
private:
	static int x;
};
 
xxx.cpp文件 
int base::x=10;//定义(初始化)时不受private和protected访问限制. 

注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使
       加上 #ifndef #define #endif 或者 #pragma once 也不行。
  1. 静态数据成员被类的所有对象所共享,包括该类派生类的对象
  2. 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以
class base{ 
public : 
	static int _staticVar; 
	int _var; 
	void foo1(int i=_staticVar);//正确,_staticVar为静态数据成员 
	void foo2(int i=_var);//错误,_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 
using namespace std;
class A {
public:
	static int m_i;
	static void foo (void) {
		cout << "foo:" << m_i << endl;
//		m_d = 3.14;//报错,静态成员函数不能访问非静态成员成员
//		bar ();    //报错,理由同上
	}
	double m_d;
	void bar (void) {
		m_i = 1000;//OK,非静态成员函数可以访问非静态成员,也可以访问静态成员
		foo ();   //OK,理由同上
	}
};
int A::m_i = 1;//在外部定义
int main (void) {
	A::m_i = 10;//通过类访问静态成员变量

	A a1, a2;
	cout << ++a1.m_i << endl;//通过对象访问静态成员变量,输出为“11”
	cout << a2.m_i << endl;  //因为静态成员变量,为多个对象共享,只有一个实例,所以上面a1将m_i修改为11,则通过a2访问的m_i也是11,输出为“11”

	A::foo ();//输出为“foo:11”,通过类访问静态成员函数
	a1.foo ();//输出为“foo:11”,通过对象访问静态成员函数
	a1.bar ();//先调用bar,将m_i修改为1000,再调用foo,输出为“foo:1000”
	return 0;
}

例子1:单例模式(饿汉方式):
#include 
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 ();//不会创建新的对象,返回已经创建好的Singletion类对象,并没有新分配内存和地址
	Singleton& s2 = Singleton::getInst ();//与上面一样,还是返回那个对象,内存地址没有变
	Singleton& s3 = Singleton::getInst ();//还是一样滴
	cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
//输出的都是0x804a0d4
	return 0;
}

例子2:单例模式(懒汉模式)
#include 
using namespace std;
// 懒汉方式
class Singleton {
public:
	static Singleton& getInst (void) {
		if (! m_inst)   //m_inst指针被初始化为NULL,第一次调用时,执行下面那一行代码来分配内存创建一个对象。否则跳过那一行代码。一旦执行过一次下一行代码,则m_inst就不会再为NULL,也就是说只会分配一次内存,只有一个对象
		m_inst = new Singleton;//分配内存,创建对象,执行一次
		++m_cn;//调用一次该函数,则数量加一
		return *m_inst;//返回创建好的m_inst
	}
	void releaseInst (void) {
		if (m_cn && --m_cn == 0)//调用了几次getInst,就要调用几次该函数,才能真正把对象释放掉
			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;//创建对象并初始化,调用构造,这个变量什维利计算m_inst的数量
int main (void) {
	Singleton& s1 = Singleton::getInst ();//第一次调用getInst函数,所以执行哪一行代码来分配内存创建对象,并返回该对象
	Singleton& s2 = Singleton::getInst ();//因为不是第一次调用,m_inst已经不再指向NULL,所以根据条件语句,不执行创建对象的代码,直接返回原先第一次调用还函数时创建好的对象,地址神马的都不变
	Singleton& s3 = Singleton::getInst ();//同上
	cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
//输出的地址都是一样的,因为只分配了一次内存,创建了一次对象
	s3.releaseInst ();//调用时,m_cn为3,结束时为2,没有释放掉
	s2.releaseInst ();//调用时,m_cn为2,结束时为1,没有释放掉
	s1.releaseInst ();//调用时,m_cn为1,满足条件语句,执行delete this ,真正释放掉
	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; //年龄
};
/*继承后父类的Person的成员(成员函数+变量)都会变成子类的一部分。这里
体现出了Student和Teacher复用了Person的成员。*/
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 
 
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成员,在派生类中成员不可直接使用的成员
    用图像表示就是如下图
    c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞_第4张图片

  • 在任何继承方式中,除了基类的private成员外,我们都可以在派生类中分别调整其访问控制,如下述例子可得

class A
{
  public:
    void f1();
    void f2();
    void f3();
  protected:
    void g1();
    void g2();
    void g3();
}

class B: private A
{
  public:
    A::f1;//把f1调整为public
    A::g1;//把g1调整为public,是否允许弱化基类的访问控制要视具体的实现而定
  protected:
    A::f2;//把f2调整为protected
    A::g2;//把g2调整为protected
}

class C: class B
{
  public:
    void h()
    {
        f1(); f2(); g1(); g2();//OK
        f3(); g3(); //Error,此时f3,g3是基类B的private成员
    }
}

调整的格式一般都是 [public: | protected: | private: ] <基类名>:: <基类成员名>;
c++学习纲要(入门必看!!!学习笔记【建议收藏!!!】)怒肝整理数万字,只求君一赞_第5张图片

  • 派生类的构造
  • 派生类是可以访问基类保护的数据成员,但是还有一些私有数据成员,派生类是无法访问的,并且为提醒类的独立性,我们还是希望通过调用基类的成员函数去初始化这些成员变量,所以派生类是通过调用基类的构造函数,实现对成员变量的初始化。具体代码示例,见上。

子类的构造函数和析构函数

  1. 子类隐式地调用基类的构造函数
    在子类的构造函数中没有显示地指明其基类部分如何构造,隐式地调用基类的无参构造函数。如果子类没有定义任何构造函数,其缺省无参构造函数同样会隐式地调用基类的无参构造函数。
  2. 子类显式地调用基类的构造函数
    在子类构造函数的初始化表中指明其基类部分的构造方式。
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.继承链的构造和初始化顺序
任何时候子类中基类子对象的构造都要先于子类构造函数中的代码。

  1. delete一个指向子类对象的基类指针,实际被执行的基类的析构函数,基类的析构函数不会调用子类析构函数,因此子类所特有的资源将形成内存泄漏。
    Human* p = new Student (…);
    delete p; // ->Human::~Human()
    delete static_cast §;
  • 子类的拷贝构造和拷贝赋值
    子类的缺省拷贝构造和拷贝赋值除了复制子类的特有部分以外,还会复制其基类部分。如果需要自己定义子类的拷贝构造和拷贝赋值,一定不要忘记在复制子类特有部分的同时,也要复制其基类部分,否则将无法得到完整意义上的对象副本。

  • 私有继承和保护继承

用于防止或者限制基类中的公有接口被从子类中扩散。

class DCT {
public:
  void codec (void) { ... }
};
class Jpeg : protected DCT { //只有自己类内可以用
public:
  void render (void) {
    codec (...);
  }
};
Jpeg jpeg;
jpeg.codec (...); // ERROR !防止公有接口被从子类中扩散

Jpeg Has A DCT,实现继承

class Jpeg2000 : public Jpeg {
public:
  void render (void) {
    codec (...); // OK ,code在Jpeg类中是保护型的,而通过公有继承,可以访问
  }
};

例子:

#include 
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) {} //正确的构造函数创建Student类的对象的同时先创建基类Huamn类,所以会调用Human类的构造函数,这里指定Human所调用的构造函数与所写的构造函数相匹配,所以不会出错。
	/*
  Student (const string& name, int age, int no) :
		 m_no (no) {
      m_name=name;
      m_age=age;
  }
  */ 错误的构造函数会报错,原因是这里调用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;//如果没有这句,则Human的eat与这里的eat作用域不再一起,则下面的eat不会与Human中的eat构成重载,而是构成隐藏关系
                    //但是有了这句之后,将Human的eat在这里可见,既作用域也被声明在这里,则两个eat构成了重载关系
	
    void eat (void) const {
		cout << "我绝食!" << endl;
	}
//	int eat;
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");
//	h1 -> learn ("C");//基类的指针或对象不可以访问子类中的成员
  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 (...); // ERROR !
//Jpeg Has A DCT,实现继承
class Jpeg2000 : public Jpeg {
public:
  void render (void) {
    codec (...); // OK !
  }
};

**

  • 多重继承

**
从多于一个基类中派生子类。
电话 媒体播放器 计算机
\ | /
智能手机

  1. 多重继承的语法和语义与单继承并没有本质的区别,只是子类对象中包含了更多的基类子对象。它们在内存中按照继承表的先后顺序从低地址到高地址依次排列。
  2. 子类对象的指针可以被隐式地转换为任何一个基类类型的指针。无论是隐式转换,还是静态转换,编译器都能保证特定类型的基类指针指向相应类型基类子对象。但是重解释类型转换,无法保证这一点。
  3. 尽量防止名字冲突。
例子:智能手机
#include 
using namespace std;
class Phone {                    //基类1
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 {                 //基类2
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 {          //基类3
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;  //将Phone中的foo函数的作用域声明到这里
	using Player::foo; //将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;  //地址都相同,但如果不用reinterpret的话,用隐式或者静态转换,p1 p2 p3将sp地址段一分为三,所以p1 p2 p3 地址会不同
	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 
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 { //virtual是虚继承
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) {}//真正起作用的是A(i),不用B(i),C(i),但要写
};
int main (void) {
	D d (1000);//B里的m_i与C里的m_i都存的是2000
	cout << d.get () << endl; // 1000,调用C中的get,返回C中的m_i的值
	d.set (2000);//调用B类中的set,给B中的m_i赋值
	cout << d.get () << endl; // 输出为2000,---如果B,C没有virtual调用C中的get,D的初始化表中没有A(i),返回C中的m_i的值,则会输出1000.----因为有了,所以制定从公共基类中虚继承,所以在最终子对象中只有一份公共基类子对象的实例
//B b(3000);  //B创建的对象没有钻石结构,所以写了virtual也不起作用,依然拥有A的基类子对象
	return 0;
}

二十一、C++多态

C++三特性:继承,封装,多态。继承和封装都有所接触的情况下,下一步就是了解多态了。

那什么是多态呢?

多态就是多种状态,就是在完成某个行为时,当不同的对象去完成时会有不同的状态。

  • C++的多态分为静态多态与动态多态。

  • 静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。

  • 动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

    • 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
      如果对象类型是子类,就调用子类的函数;如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。

多态实现的三个条件:

  1. 必须是公有继承

  2. 必须是通过基类的指针或引用 指向派生类对象 访问派生类方法

  3. 基类的方法必须是虚函数,且完成了虚函数的重写

多态分为静态多态(编译阶段)和动态多态(运行阶段)

  • 静态多态:函数重载和泛型编程
  • 动态多态:虚函数 :根据绑定的类型调用响应的函数执行!

动态多态依靠虚函数来实现:动态多态三要素:

  • 父类有虚函数;
  • 子类改写了虚函数;
  • 通过父类的指针或引用来调用虚函数, 在运行时,绑定到不同的子类中,产生不同的行为

图形:位置,绘制
/
矩形:宽和高 圆:半径
绘制 绘制

直接上例子:图形绘制

#include 
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) {    //隐藏shape中的draw,构成隐藏关系
		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 ();}
//因为在shape中的draw()有virtual修饰为虚函数,而另外两个子类中的同名draw也变为虚函数,覆盖了基类shape中的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只能代表shape代表不了rect。
shape->draw ();    // Shape::draw
Shape& shape = rect;
shape->draw ();    // Rect::draw
-------------------------------------------------
class A {
public:
  A (void) {
    bar (); // A::bar   //构造函数中调用虚函数,永远没有多态型,构造A的时候,B还没有构造好,没法调用B中的尚未构造好的覆盖版本。
  }
  ~A (void) {
    bar (); // A::bar  //析构函数中调用虚函数,永远没有多态性,因为析构的顺序和构造相反,当执行基类中的析构函数时,子类已经析构后释放完了,无法调用析构后的覆盖版本
  }
  void foo (void) {
    This->bar (); // B::bar
  }
  virtual void bar (void) {
    cout << 'A' << endl;
  }
};
class B : public A {
  void bar (void) {
    cout << 'B' << endl;
  }
};
int main (void) {
  B b; // A
  b.foo (); // B   因为foo函数是A类中的成员函数,所以this指针是A类型的,这个this指针指向B类型的对象b,调用那个虚函数的覆盖版本看指针指向的目标对象,所以调用B中的bar
  return 0;
}

二十三、C++ 函数覆盖的条件

overload - 重载
override - 覆盖、重写、改写

  1. 基类版本必须是虚函数。
  2. 函数名、形参表和常属性必须严格一致。
  3. 如果返回基本类型或者对象,那么也必须严格一致。如果返回类类型的指针或引用,那么子类版本也可以返回基类版本的子类。
    class B : public A { … };
    基类:virtual A* foo (void) {…}
    子类:A* foo (void) { … }
    B* foo (void) { … }
  4. 子类的覆盖版本不能比基类版本声明更多的异常抛出。
  5. 子类覆盖版本的访控属性与基类无关。
class A {
public:
  virtual void foo (void) { ... }
};
class B : public A {
private:
  void foo (void) { ... }
};
int main (void) {
  B* b = new B;
  b->foo (); // ERROR !foo在B中是私有的
  A* a = new B;
  a->foo (); // OK ! -> B::foo  访控属性是看指针类型的,在A中,foo 是公共部分的,所以可以访问,但真正执行的是覆盖版本的B中的foo。
}

二十二、C++ 纯虚函数

  • 什么是虚函数?
    在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
    就像如下,定义一虚函数
#include 
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;
    }
}

即形如:

  • virtrual 返回类型 成员函数名 (形参表) = 0;
    的虚函数被称为纯虚函数
  • 注意
    一个包含了纯虚函数类称为抽象类,抽象类不能实例化为对象。
    如果一个类继承自抽象类,但是并没有为其抽象基类中的全部纯虚函数提供覆盖,那么该子类就也是一个抽象类。
    (1)纯虚函数没有函数体;
    (2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
    (3)这是一个声明语句,最后有分号。
    纯虚函数只有函数的名字而不具备函数的功能,不能被调用。
    纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。
    如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。

抽象类
不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。
纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

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 
using namespace std;
class Shape {
public:
	Shape (int x, int y) : m_x (x), m_y (y) {}
	virtual void draw (void) = 0;  //空函数,纯虚函数,因为有了这个纯虚函数,所以shape为抽象类。所以shape不能实例化,不能创建对象,如果该类中除了构造和析构函数之外,都是纯虚函数,则该类为纯抽象类,同样不能实例化。
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;
	}
//      int draw (void){}         //不构成任何合法关系
//	int draw (void) const {}  //会隐藏,因为形参不同(这里的this是const类型)
//	int draw (int){}          //隐藏
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);
//	Shape shape (1, 2);
	return 0;
}

二十三、C++ 动态绑定(后期绑定、运行时绑定)

  1. 虚函数表
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 (); // A::foo
pa->bar (); // A::bar
---------------------
A* pa = new B;
pa->foo (); // B::foo
pa->bar (); // A::bar
  1. 动态绑定
    当编译器看到通过指向子类对象的基类指针或者引用子类对象的基类引用,调用基类中的虚函数时,并不急于生成函数调用代码,相反会在该函数调用出生成若干条指令,这些指令在程序的运行阶段被执行,完成如下动作:
    1)根据指针或引用的目标对象找到相应虚函数表的指针;
    2)根据虚函数表指针,找到虚函数的地址;
    3)根据虚函数地址,指向虚函数代码。
    由此可见,对虚函数的调用,只有运行阶段才能够确定,故谓之后期绑定或运行时绑定。
  2. 动态绑定对程序的性能会造成不利影响。如果不需要实现多态就不要使用虚函数。
例子:
#include 
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;   //a
	void (**vft) (void) = *(void (***) (void))&a;  //使vft(二级指针)指向a的虚函数表,因为a是A类型的,所以强制转换为void(***),虚函数表是一个函数指针数组,要指向他,应该用指向指针的指针--二级指针。
	cout << (void*)vft[0] << ' '
		<< (void*)vft[1] << endl; //A类中foo函数与bar函数的地址
	vft[0] ();//调用了A类的foo
	vft[1] ();//调用了A类的bar

	B b;
	vft = *(void (***) (void))&b;//使vft指向B类的虚函数表
	cout << (void*)vft[0] << ' '
		<< (void*)vft[1] << endl;//B类中的foo函数与A类中的bar函数地址
	vft[0] ();//调用了B类的foo函数
	vft[1] ();//调用了A类的bar函数
	return 0;
}

二十四、运行时类型信息(RTTI)

1.typeid操作符

A a; typeid (a)返回typeinfo类型的对象的常引用。 typeinfo::name() - 以字符串的形式返回类型名称。
typeinfo::operator==() -类型一致 typeinfo::operator!=() -类型不一致
#include

例子:

#include 
#include 
#include 
using namespace std;
class A {
public:
	virtual void foo (void) {}
};
class B : public A {};
void print (A* pa) {
    //	if (! strcmp (typeid (*pa).name (), "1A"))
	if (typeid (*pa) == typeid (A))
		cout << "pa指向A对象!" << endl;
	else
    //	if (! strcmp (typeid (*pa).name (), "1B"))
	if (typeid (*pa) == typeid (B))
		cout << "pa指向B对象!" << endl;
}
int main (void) {
	cout << typeid (int).name () << endl;           //'i'
	cout << typeid (unsigned int).name () << endl;  //'j'
	cout << typeid (double[10]).name () << endl;    //A10_d
	cout << typeid (char[3][4][5]).name () << endl; //A3_A4_A5_c
	char* (*p[5]) (int*, short*);                   //函数指针数组
	cout << typeid (p).name () << endl;             //A5_PFPcPiPsE
	cout << typeid (const char* const* const).name (//
		) << endl;     //PKPKc  指针指向一个常量,这个常量是个指针,这个指针指向一个常量,这个常量是char类型的。
	cout << typeid (A).name () << endl;//1A
	A* pa = new B;
	cout << typeid (*pa).name () << endl;//A中有虚函数,所以'1B’,如果A中没有虚函数,则是‘1A’。没有多态,则会按照指针本身的类型,有多态则会按照指针指向的目标对象类型。
	print (new A);//“pa指向A对象”
	print (new B);//“pa指向B对象”
}

2.dynamic_cast
例子:

#include 
//动态类型转换
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;
                  //动态类型转换,运行期间检查。
	// A是B的基类,pa指向B对象,成功
	B* pb = dynamic_cast<B*> (pa);
	cout << pb << endl;    //地址
	// A不是C的基类,pa没有指向C对象,失败,安全
	C* pc = dynamic_cast<C*> (pa);
	cout << pc << endl;    // 0
	A& ra = b;     //引用子类对象的基类引用。
	try {
		C& rc = dynamic_cast<C&> (ra);
	}
	catch (exception& ex) {
		cout << "类型转换失败:" << ex.what ()
			<< endl;
		// ...
	}
	// pa没有指向D对象,失败,安全
	D* pd = dynamic_cast<D*> (pa);
	cout << pd << endl;
	
        cout << "-------- sc --------" << endl;
	       //静态类型转换,编译期间检查。
           // B是A的子类,成功
	pb = static_cast<B*> (pa);
	cout << pb << endl;
	// C是A的孙子类,成功,危险!
	pc = static_cast<C*> (pa);
	cout << pc << endl;

	 // D不是A的后裔,失败,安全
     //	pd = static_cast (pa);   //两个方向都不能做隐式转换,所以任何方向也不能做静态转换。
     //	cout << pd << 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;  }

你可能感兴趣的:(笔记,c++,考试不挂科,c++)