引用:别名,本质上是指针常量
- 语法: 类型(与原名类型必须一致) &别名 = 原名
- 引用必须在声明时初始化,且一旦初始化后,就不可以引向其他变量
- 建立对数组引用
- 直接建立引用
int arr[10];
int(&pArr)[10] = arr;
- 先定义出数组类型,再通过类型定义引用
typedef int(ARRAY_TYPE)[10];
ARRAY_TYPE & pArr2 = arr;
- 参数的传递方式
- 值传递
- 地址传递
- 引用传递
- 注意事项
- 引用必须引一块合法内存空间
- 不要返回局部变量的引用
- 当函数返回值是引用时候,那么函数的调用可以作为左值进行运算
- 注意事项
func()=1000;
int func(int a, int b = 10 , int c = 10)
- 注意事项 ,如果有一个位置有了默认参数,那么从这个位置以后都必须有默认值(其前可以没有)
- 函数的声明和实现只能有一个 提供默认参数,不可以同时加默认参数
- 占位参数
- 只写一个类型进行占位,调用时候必须要传入占位值
void func2(``**int a**`` , int = 1)
- 满足条件
- 同一个作用域下
- 函数名称相同
- 函数参数个数、类型、顺序不同
- 函数的返回值 不可以作为重载条件
注意事项
用途:在C++中调用C语言文件
C++中有函数重载,会对函数名称做修饰,导致调用C语言的函数链接失败
利用extern C可以解决问题
ifdef cplusplus // 两个下划线 c plus plus
}
endif
封装
- C语言的封装
- 缺陷 将属性和行为分离
- C++语言的封装
- 将属性和行为作为一个整体,来表现生活中的事物
- 将属性和行为 加以权限控制
- 访问权限
- 公共权限 public 类内 类外 都可以访问(无视作用域,可以直接通过对象访问成员函数内的成员)
- 私有权限 private 类内可以访问 类外不可以访问
- 保护权限 protected类内可以访问 类外不可以访问,但派生类可以访问基类
- 默认权限
- class 私有权限
- struct 公共权限
尽量将成员属性设置为私有
构造函数
- 没有返回值 不用写void
- 函数名与 类名相同
- 可以有参数 ,可以发生重载
- 构造函数由编译器自动调用一次 无须手动调用
- 析构函数
- 没有返回值 不用写void
- 函数名 与类名相同 函数名前 加 ~
- 不可以有参数 ,不可以发生重载
- 析构函数 也是由编译器自动调用一次,无须手动调用
- 构造函数的分类和调用
- 分类
- 按照参数分类
- 有参
- 无参(默认)
- 按照类型分类
- 普通构造函数
- 拷贝构造
- 写法:参数为引用
const Person & p;编译器会默认隐式实现 - 使用场景
- 用已经创建好的对象来初始化新的对象
- 值传递的方式 给函数参数传值
- 以值方式 返回局部对象
- 写法:参数为引用
- 按照参数分类
- 三种调用方法
- 括号法
Person p1(10);- 不要用括号法 调用无参构造函数
Person p3();编译器认为代码是函数的声明
- 不要用括号法 调用无参构造函数
- 显示法
Person p2 = Person(10)- “匿名对象”
- 单独调用
Person(10); - 特点: 当前行执行完后 立即释放
- 不要用拷贝构造函数初始化匿名对象
- 单独调用
- “匿名对象”
- 括号法
- 分类
Person(p3); 编译器认为 Person p3对象实例化 如果已经有p3 p3就重定义
- **隐式法**`Person p5 = 10;`- 相当于`Person p5 = Person(10);` 但可读性低
- 构造函数的调用规则
- 编译器会给一个类至少添加3个函数 默认构造(空实现) 析构函数(空实现) 拷贝构造(值拷贝)
- 如果我们自己提供了有参构造函数,编译器就不会提供默认构造函数,但是依然会提供拷贝构造函数
- 如果我们自己提供了拷贝构造函数,编译器就不会提供其他构造函数
- 深拷贝与浅拷贝的问题以及解决
- 如果有属性开辟到堆区,利用编译器提供拷贝构造函数会调用浅拷贝带来的析构重复释放堆区内存的问题
- 利用深拷贝解决浅拷贝问题
- 自己提供拷贝构造函数,实现深拷贝
- 初始化列表
- 可以利用初始化列表语法对类中属性进行初始化
- 语法:构造函数名称后 : 属性(值), 属性(值)…
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c)
- 类对象作为类中成员
- 当其他类对象 作为本类成员,先构造其他类对象,再构造自身,析构的顺序和构造相反
- explicit关键字
- explicit用途:防止利用隐式类型转换方式来构造对象(构造类型调用的方法三:隐式)
- new和delete
- malloc 和 new 区别
- malloc 和 free 属于 库函数 new 和delete属于 运算符
- malloc不会调用构造函数 new会调用构造函数
- malloc返回void* C++下要强转 new 返回创建的对象的指针
- 注意事项 不要用void去接受new出来的对象,利用void无法调用析构函数
- 利用new创建数组
- Person * pPerson = new Person[10];
- 释放数组时候 需要加[]
- delete [] pPerson;
- 堆区开辟数组,一定会调用默认构造函数
- 栈上开辟数组,可不可以没有默认构造,可以没有默认构造
Chp 4
- 静态成员
- 静态成员变量
- 所有对象都共享同一份数据
- 编译阶段就分配内存
- 类内声明、类外初始化
- 访问方式有两种:通过对象访问、通过类名访问
- 静态成员变量也是有访问权限
- 静态成员函数
- 所有对象都共享同一份函数
- 静态成员函数 只可以访问 静态成员变量,不可以访问非静态成员变量
- 静态成员函数 也是有访问权限的
- 静态成员函数 有两种访问方式:通过对象 、通过类名
- 单例模式 – 主席类案例
- 通过一个类 只能实例化唯一的一个对象
- 私有化
- 默认构造
- 拷贝构造
- 唯一实例指针
- 对外提供 getInstance 接口,将指针返回
- 单例模式 – 打印机案例
- 和主席类案例一样设计单例模式
- 提供打印功能并且统计打印次数
- C++对象模型初探
- 类中的成员变量 和 成员函数 是分开存储的
- 只有非静态成员变量 属于类对象上
- 空类的sizeof结果 1
- this指针
- this指针指向 被调用的成员函数所属的对象
- this指针可以解决名称冲突
- this指针隐式加在每个成员函数中
- *this 就是本体
- p1.personAddPerson(p2).personAddPerson(p2).personAddPerson(p2); //链式编程
- 空指针访问成员函数
- 如果成员函数中没有用到this指针,可以用空指针调用成员函数
- 如果成员函数中用到了this,那么这个this需要加判断,防止代码down掉
- 常对象和常函数
- 常函数
- 成员函数 声明后面加const
- void showPerson() const
- const目的是为了修饰成员函数中的this指针,让指针指向的值不可以修改
- 有些属性比较特殊,依然在常函数或者常对象中可以修改,需要加入关键字 mutable
- 常对象
- const Person p
- 常对象也不许修改成员属性
- 常对象只能调用常函数
- 对于成员函数 ,可不可以 用static 和 const同时修饰 ,不可以
- 友元
- 全局函数作为友元函数
- 利用friend关键字让全局函数 goodGay作为本类好朋友,可以访问私有成员
- friend void goodGay(Building * buliding);
- 类作为友元类
- 让goodGay类作为 Building的好朋友,可以访问私有成员
- friend class GoodGay;
- 类中的成员函数作为友元函数
- //让GoodGay类中的 visit成员函数作为友元
- friend void GoodGay::visit();
Chp 5
强化训练-数组类封装
设计类 myArray
属性
int m_Capacity数组容量
int m_Size 数组大小
int pAddress 维护真实在堆区创建的数组指针
行为
默认构造
有参构造
拷贝构造
析构
根据位置 设置数据
根据位置 获取数据
尾插
获取数组容量
获取数组大小
加号运算符重载
对于内置的数据类型,编译器知道如何进行运算
但是对于自定义数据类型,编译器不知道如何运算
利用运算符重载 可以让符号有新的含义
利用加号重载 实现p1 + p2 Person数据类型相加操作
利用成员函数 和 全局函数 都可以实现重载
关键字 operator +
成员本质 p1.operator+(p2)
全局本质 operator+(p1,p2)
简化 p1 + p2
运算符重载 也可以发生函数重载
左移运算符重载
不要滥用运算符重载,除非有需求
不能对内置数据类型进行重载
对于自定义数据类型,不可以直接用 cout << 输出
需要重载 左移运算符
如果利用成员 函数重载 ,无法实现让cout 在左侧,因此不用成员重载
利用全局函数 实现左移运算符重载
ostream& operator<<(ostream &cout, Person & p1)
如果想访问类中私有内存,可以配置友元实现
递增运算符重载
前置递增
MyInter& operator++()
后置递增
MyInter operator++(int)
前置++ 效率高于 后置++ 效率,因为后置++会调用拷贝构造,创建新的数据
指针运算符重载
智能指针
用途: 托管new出来的对象的释放
设计smartPoint智能指针类,内部维护 Person ,在析构时候释放堆区new出来的person对象
重载 -> 让 sp智能指针用起来向真正的指针
赋值运算符重载
编译器会默认个一个类添加4个函数
默认构造、析构 、 拷贝构造(值拷贝) 、 operator=(值拷贝)
如果类中有属性创建在堆区,利用编译器提供的 = 赋值运算就会出现堆区内存重复释放的问题
解决方案:利用深拷贝 重载 =运算符
Person& operator=( const Person &p)
[]运算符重载
int& operator;
实现访问数组时候利用[] 访问元素
Chp 6
关系运算符重载
对于自定义数据类型,编译器不知道如果进行比较
重载 == !=号
bool operator==( Person & p)
bool operator!=(Person & p)
函数调用运算符重载
重载 ()
使用时候很像函数调用,因此称为仿函数
void operator()(string text)
int operator()(int a,int b)
仿函数写法不固定,比较灵活
cout << MyAdd()(1, 1) << endl; // 匿名函数对象 特点:当前行执行完立即释放
不要重载 && 和 ||
原因是无法实现短路特性
建议:将<< 和 >>写成全局函数,其他可重载的符号写到成员即可
强化训练-字符串类封装
myString类实现自定义的字符串类
属性
char pString; 维护 在堆区真实开辟的字符数组
int m_Size; 字符串长度
行为
有参构造 MyString(char str)
拷贝构造 MyString(const MyString & str);
析构 ~MyString();
重载<< 运算符
重载 >> 运算符
重载 = 赋值运算
重载 [] str[0] 按照索引位置设置获取字符
重载 + 字符串拼接
重载 == 对比字符串
继承基本语法
继承优点:减少重复的代码,提高代码复用性
// 语法: class 子类 : 继承方式 父类
// News 子类 派生类
// BasePage 父类 基类
继承方式
公共继承
父类中公共权限,子类中变为公共权限
父类中保护权限,子类中变为保护权限
父类中私有权限,子类访问不到
保护继承
父类中公共权限,子类中变为保护权限
父类中保护权限,子类中变为保护权限
父类中私有权限,子类访问不到
私有继承
父类中公共权限,子类中变为私有权限
父类中保护权限,子类中变为私有权限
父类中私有权限,子类访问不到
继承中的对象模型
父类中的私有属性,子类是继承下去了,只不过由编译器给隐藏了,访问不到
可以利用开发人员工具查看对象模型
C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts
打开开发人员命令工具
跳转盘符 E:
跳转文件路径 cd到文件路径下
cl /d1 reportSingleClassLayout类名 文件名
继承中的构造和析构
先调用父类构造,再调用其他成员构造,再调用自身构造 ,析构的顺序与构造相反
利用初始化列表语法 显示调用父类中的其他构造函数
父类中 构造、析构、拷贝构造 、operator= 是不会被子类继承下去的
继承中的同名成员处理
我们可以利用作用域访问父类中的同名成员
当子类重新定义了父类中的同名成员函数,子类的成员函数会隐藏掉父类中所有重载版本的同名成员,可以利用作用域显示指定调用
继承中的同名 静态成员处理
结论和 非静态成员 一致
只不过调用方式有两种
通过对象
通过类名
通过类名的方式 访问父类作用域下的m_A静态成员变量
Son::Base::m_A
多继承基本语法
class 子类: 继承方式 父类1 ,继承方式 父类2
当多继承的两个父类中有同名成员,需要加作用域区分
菱形继承
两个类有公共的父类 和共同的子类 ,发生菱形继承
菱形继承导致数据有两份,浪费资源
解决方案:利用虚继承可以解决菱形继承问题
class Sheep : virtual public Animal{};
//当发生虚继承后,sheep和tuo类中继承了一个 vbptr指针 虚基类指针 指向的是一个 虚基类表 vbtable
//虚基类表中记录了 偏移量 ,通过偏移量 可以找到唯一的一个m_Age
利用地址偏移找到 vbtable中的偏移量 并且访问数据
Chp 7
静态联编动态联编
静态多态和动态多态
静态多态:函数重载,运算符重载
动态多态:
//先有继承关系
//父类中有虚函数,子类重写父类中的虚函数
//父类的指针或引用 指向子类的对象
静态多态在编译阶段绑定地址,地址早绑定,静态联编
动态多次在运行阶段绑定地址,地址晚绑定,动态联编
多态原理
当父类写了虚函数后,类内部结构发生改变,多了一个vfptr
vfptr 虚函数表指针 —— > vftable 虚函数表
虚函数表内部记录着虚函数的入口地址
当父类指针或引用指向子类对象,发生多态,调用是时候从虚函数中找函数入口地址
虚函数 关键字 virtual
利用指针的偏移调用函数
((void()()) ((int )(int )animal)) ();
typedef void( __stdcall FUNPOINT)(int);
(FUNPOINT (((int)(int)animal + 1)))(10);
多态案例 - 计算器案例
设计抽象计算器类,分别实现加减乘计算,继承于抽象计算器类,重写虚函数
利用多态可以调用不同计算器
多态的好处
代码可读性强
组织结构清晰
扩展性强
开闭原则: 对扩展进行开放 对修改进行关闭
纯虚函数和抽象类
语法: virtual int getResult() = 0;
//如果一个类中包含了纯虚函数,那么这个类就无法实例化对象了,这个类通常我们称为 抽象类
//抽象类的子类 必须要重写 父类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
虚析构语法:
virtual ~Animal(){}
如果子类中有指向堆区的属性,那么要利用虚析构技术在delete的时候 调用子类的析构函数
纯虚析构语法:
virtual ~Animal() = 0;
Animal::~Animal(){ .. }
//纯虚析构 需要有声明 也需要有实现
//如果一个类中 有了 纯虚析构函数,那么这个类也属于抽象类,无法实例化对象了
向上类型转换和向下类型转换
父转子 向下类型转换 不安全
子转父 向上类型转换 安全
如果发生多态,那么转换永远都是安全的
重载、重写、重定义
重载
函数重载
同一个作用域下,函数名称相同,参数个数、顺序、类型不同
重写
子类重写父类中的虚函数,函数返回值、函数名、形参列表完全一致称为重写
重定义
子类重新定义父类中的同名成员函数,隐藏掉父类中同名成员函数,如果想调用加作用域
多态案例2 - 电脑组装案例
利用多态实现 电脑组装
Chp 8
函数模板
泛型编程 – 模板技术 特点:类型参数化
template< typename T > 告诉编译器后面紧跟着的函数或者类中出现T,不要报错,T是一个通用的数据类型
实现通用两个数进行交换函数
使用
自动类型推导 必须要推导出一致的T才可以使用
显示指定类型 mySwap
实现对char和 int类型数组进行排序
利用模板技术 实现对char和int类型数组通用排序函数
函数模板和普通函数的区别以及调用规则
区别
如果使用自动类型推导,是不可以发生隐式类型转换的
普通函数 可以发生隐式类型转换
调用规则
如果函数模板和普通函数都可以调用,那么优先调用普通函数
如果想强制调用函数模板,可以使用空模板参数列表
myPrint<>(a, b);
函数模板也可以发生函数重载
如果函数模板能产生更好的匹配,那么优先使用函数模板
模板的实现机制
编译器并不是把函数模板处理成能够处理任何类型的函数
函数模板通过具体类型产生不同的函数 —- 通过函数模板产生的函数 称为模板函数
编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
模板局限性
模板并不是真实的通用,对于自定义数据类型,可以使用具体化技术,实现对自定义数据类型特殊使用
template<> bool myCompare(Person &a, Person &b)
类模板
//类模板和函数模板区别:
//1、类模板不可以使用自动类型推导,只能用显示指定类型
//2、类模板中 可以有默认参数
类模板中成员函数创建时机
类模板中的成员函数 并不是一开始创建的,而是在运行阶段确定出T的数据类型才去创建
类模板做函数参数
1、指定传入类型
void doWork(Person
2、参数模板化
template
void doWork2(Person
3、整个类模板化
template
void doWork3( T &p)
查看T数据类型
typeid(T).name()
类模板碰到继承的问题以及解决
必须要指定出父类中的T数据类型,才能给子类分配内存
template
class Son2 :public Base2
类模板中的成员函数类外实现
void Person
类模板的分文件编写问题以及解决
类模板中的成员函数,不会一开始创建,因此导致分文件编写时连接不到函数的实现,出现无法解析的外部命令错误
解决方式1:
直接包含.cpp文件 (不推荐)
解决方式2:
将类声明和实现写到同一个文件中,将文件的后缀名改为 .hpp 即可
类模板碰到友元的问题以及解决
友元类内实现
friend void printPerson(Person
友元类外实现
声明:
friend void printPerson2<>(Person
实现:
template
void printPerson2(Person
template
class Person;
template
void printPerson2(Person
类模板应用 – 数组类封装
将类写到 myArray.hpp中
属性:
T * pAddress; 指向堆区数组指针
int m_Capacity 数组容量
int m_Size ;数组大小
行为
myArray(int capacity)
myArray(const MyArray & arr)
operator=
operator[]
~myArray()
getCapacity
getSize
pushback
Chp 9
类型转换
静态类型转换 static_cast
允许内置数据类型转换
允许父子之间的指针或者引用的转换
语法 static_cast<目标类型>(原变量/原对象)
动态类型转换 dynamic_cast
不允许内置数据类型转换
允许父子之间指针或者引用的转换
父转子 不安全的 转换失败
子转父 安全 转换成功
如果发生多态,总是安全,可以成功
语法 dynamic_cast<目标类型>(原变量/原对象)
常量转换 const_cast
只允许 指针或者引用 之间转换
语法 const _cast<目标类型>(原变量/原对象)
重新解释转换
reinterpret_cast 最不安全一种转换,不建议使用
异常的基本语法
C++异常的处理关键字
try throw catch
可以出现异常的代码放到 try块
利用throw抛出异常
利用catch捕获异常
catch( 类型) 如果想捕获其他类型 catch(…)
如果捕获到的异常不想处理,而继续向上抛出,利用 throw
异常必须有函数进行处理,如果都不去处理,程序自动调用 terminate函数,中断掉
异常可以是自定义数据类型
栈解旋
从try代码块开始,到throw抛出异常之前,所有栈上的数据都会被释放掉,
释放的顺序和创建顺序相反的,这个过程我们称为栈解旋
异常的接口声明
在函数中 如果限定抛出异常的类型,可以用异常的接口声明
语法: void func()throw(int ,double)
throw(空)代表 不允许抛出异常
异常变量的生命周期
//抛出的是 throw MyException(); catch (MyException e) 调用拷贝构造函数 效率低
//抛出的是 throw MyException(); catch (MyException &e) 只调用默认构造函数效率高 推荐
//抛出的是 throw &MyException(); catch (MyException e) 对象会提前释放掉,不能在非法操作
//抛出的是 new MyException(); catch (MyException e) 只调用默认构造函数 自己要管理释放
异常的多态使用
提供基类异常类
class BaseException
纯虚函数 virtual void printError() = 0;
子类空指针异常 和 越界异常 继承 BaseException
重写virtual void printError()
测试 利用父类引用指向子类对象
系统标准异常
引入头文件 #include
抛出越界异常 throw out_of_range(“…”)
获取错误信息 catch( exception & e ) e.what();
编写自己的异常类
编写myOutofRange 继承 Exception类
重写 virtual const char what() const
将sting 转为 const char
.c_str()
const char 可以隐式类型转换为 string 反之不可以
测试,利用多态打印出错误提示信息
标准输入流
cin.get() 获取一个字符
cin.get(两个参数) 获取字符串
利用cin.get获取字符串时候,换行符遗留在缓冲区中
cin.getline() 获取字符串
利用cin.getline获取字符串时候,换行符不会被取走,也不在缓冲区中,而是直接扔掉
cin.ignore() 忽略默认忽略1个字符, 如果填入参数X,代表忽略X个字符
cin.peek() 偷窥
cin.putback() 放回放回原位置
案例1
判断用户输入的内容是字符串还是数字
案例2
用户输入 0 ~ 10 之间的数字,如果输入有误,重新输入
标志位 cin.fail() 0代表正常 1代表异常
cin.clear() cin.sync() 清空缓冲区 重置标志位
标准输出流
cout.put() //向缓冲区写字符
cout.write() //从buffer中写num个字节到当前输出流中。
通过 流成员函数 格式化输出
int number = 99;
cout.width(20); //指定宽度为20
cout.fill(‘‘); //填充
cout.setf(ios::left); //左对齐
cout.unsetf(ios::dec); //卸载十进制
cout.setf(ios::hex); //安装十六进制
cout.setf(ios::showbase); //显示基数
cout.unsetf(ios::hex); //卸载十六进制
cout.setf(ios::oct); //安装八进制
cout << number << endl;
通过控制符 格式化输出
int number = 99;
cout << setw(20) //设置宽度
<< setfill(‘~’) //设置填充
<< setiosflags(ios::showbase) //显示基数
<< setiosflags(ios::left) //设置左对齐
<< hex //显示十六进制
<< number
<< endl;
引入头文件 #include< iomanip>
文件读写
头文件 #inlcude < fstream>
写文件
ofstream ofs (文件路径,打开方式 ios::out )
判断文件是否打开成功 ofs.is_open
ofs << “…”
关闭文件 ofs.close();
读文件
ifstream ifs(文件路径,打开方式 ios::in)
判断文件是否打开成功 ofs.is_open
利用4种方式 对文件进行读取
关闭文件 ifs.close();
