蒋贵良
课程时间:
标准C++(11天)
QT(8天)
=======================
《C++程序设计原理与实现》
《C++ Primer》
=======================
联系:
[email protected]
QQ:280089088
=======================
一 C++概述
1 C++历史背景
1)C++的江湖地位
jave C C++ C# python
2)C++之父:Bjarne Stroustrup(1950--)
1979,Cpre,为C语言增加类的机制
1983,Bjarne发布了全新的编程语言C with Class
1985,CFront1.0《The C++ programming Language》
3)C++发展过程
1987,GNU C++
1990,Borland C++(BC编译器)
1992,Microsoft C++(VC)
1998,ISO C++98
2003,对C++98进行修订,C++03
2011,ISO C++11/C++0x
2014, ISO对C++标准做了部分扩展,C++14
*2017,C++17(未知)
2 应用领域
1)游戏
2)科学计算
3)网络通信(ACE)
4)操作系统和设备驱动
5)其它...
3 C和C++
1)都是编译型语言
2)都是强类型语言,但是C++更强
3)C++兼容C语言,但是去除了C中不好的特性
4)C++增加了很多了好的特性,比C语言更适合大型软件的开发
二 第一个C++程序
1 编译方式
1)gcc xx.cpp -lstdc++
2)g++ xx.cpp //good
2 文件扩展名
1)xx.cpp//good
2)xx.cc
3)xx.cxx
4)xx.C
3 头文件
//包含标准C++中所有和I/O有关的类型、对象、函数
#include <iostream>
//在C++中依然可以使用C库的函数,但需要写上对应的有文件,C++中提供了一套和C库对应的头文件
#include <stdio.h> ==> #include <cstdio>
#include <stdlib.h> ==> #include <cstdlib>
#include <string.h> ==> #include <cstring>
...
4 标准输入和输出
1)用cin对象表示标准输入//类似scanf
eg:
//从标准输入设备获取一个整形数放到变量a中
int a;
scanf("%d",&a);
cin >> a;
">>" 称为提取运算符
eg:
int a;
double d;
scanf("%d%lf",&a,&d);
cin >> a >> d;
2)用cout对象表示标准输出//类似printf
eg:
int a = 100;
printf("%d\n",a);
cout << a << endl;
"<<":称为插入运算符
注:endl表示换行,和“\n”等价
eg:
int a = 10;
double d = 3.14;
printf("%d,%lf\n",a,d);
cout << a << ‘,‘ << d << endl;
5 "std::"表示标准名字空间
三 名字空间(namespace)
1 名字空间的作用
1)避免名字冲突
2)划分逻辑单元
2 定义名字空间
namespace 名字空间名{
名字空间成员1;
名字空间成员2;
...
}
名字空间成员可以是全局变量、全局函数、类型、名字空间。
eg:
namespace ns{
int i;//全局变量
void func(void){...}//全局函数
struct Stduent{...};//结构体类型
namespace ns2{...}
}
3 名字空间成员使用
1)通过作用域限定运算符“::”
名字空间名::要访问的成员;
eg:
namespace ns{
int i;//全局变量
}
int main(void){
i=100;//error,名字空间里面成员不能直接访问
ns::i=100;//ok,通过作用域限定符可以访问
}
2)名字空间指令
using namespace 名字空间名;
在该条指令以后的代码,指定名字空间中的成员都可见,访问其中的成员可以省略作用域限定。
3)名字空间声明
using 名字空间名::名字空间成员;
将名字空间中的某个成员引入当前作用域,在该作用域访问这个成员如果访问局部变量一样,可以省略作用域限定.
eg:
namespace ns{
int i1=10;
int i2=20;
}
int main(void){
using namespace ns;//名字空间指令
cout << i1 << endl;//ok
cout << i2 << endl;//ok
---------------------------
using ns::i1;//名字空间声明
cout << i1 << endl;//ok
cout << i2 << endl;//error
}
4 无名名字空间
不属于任何名字空间的标识符,将被编译器划分到无名名字空间中,显式访问里面的成员:"::成员"。
5 嵌套名字空间//了解
eg:
namespace china{
namespace beijing{
namespace chaoyang{
char* name;
}
}
}
china::beijing::chaoyang::name = "老王";
四 C++结构体、联合体和枚举
1 C++结构体
1)定义结构体类型变量时可以省略“struct”关键字
eg:
struct A{...};//声明
A a;//定义结构体类型的变量a
2)在C++结构体里面可以直接定义函数,称为成员函数,而且在成员函数中可以直接访问成员变量;
eg:
struct A{
int a;//成员变量
void foo(void){...}//成员函数
};
2 联合体 //了解
1)定义联合体类型变量时可以省略“union”关键字
2)支持匿名联合
3 枚举
1)定义枚举类型变量时可以省略“enum”关键字
2)C++枚举是一种独立的数据类型,而C中枚举本质就是整型数。
eg:
enum COLOR{RED,GREEN,BLUE};
/*enum*/COLOR c;
c = 100;//C:ok C++:error
五 C++的字符串
1 C++兼容C中字符串表示方式
1)字面值常量字符串 "hello"
2)字符指针 char*
3)字符数组 char[]
eg:
const char* p = "hello";
//strcpy(p,"world");//段错误
p = "world";//ok
char arr[5]={0};
strcpy(arr,"jiangguliang");//越界使用内存危险
cout << arr << endl;
//arr-->char* const arr
//arr = "hello";//error
2 C++中增加string类型,专门表示字符串
1)定义字符串
string s;//定义空字符串
string s1 = "hello";//定义同时初始化
string s2("hello");
string s3 = string("hello");
注:s1 s2 s3三种写法完全等价
2)字符串的基本操作
--》字符串拷贝"="
--》字符串的连接"+" "+="
--》字符串比较: > < == !=
--》获取字符串中某个字符:[]
--》获取字符串的长度:size()/length()
--》将string转换成char*: c_str()
eg:
string s1 = "hello";
s1 = "abcdefg";//拷贝字符串
cout << s1 << endl;//abcdefg
------------------------------
string s1 = "hello";
s1 += " world";//把world连接到s1后面
cout << s1 << endl;//"hello world"
------------------------------
string s1 = "hello";
string s2 = s1 + " world";
cout << s2 << endl;//"hello world"
------------------------------
string s1 = "hello";
string s2 = "world";
if(s1 > s2){//字符串比较
cout << "s1>s2" << endl;
}
else{
cout << "s1<s2" << endl;
}
------------------------------
string s1 = "hello";
//获取字符串中某个字符
cout << s1[0] << endl;//h
s1[0] = ‘H‘;
cout << s1 << endl;//Hello
------------------------------
string s1 = "hello world!";
cout << s1.size() << endl;//12
cout << s1.length() << end;//12
-----------------------------
string s1 = "hello";
char* s2 = s1;//error
char* s2 = s1.c_str();//ok
=====================
练习:使用string表示字符串,从键盘读取一个字符串,统计里面包含字母A/a的个数.
string s;
cin >> s;//注:会被空白字符截断
getline(cin,s);//它可以读走空格
--day02--
回顾:
1 C++标准
C++98、C++11/C++0x
2 第一个C++程序
.cpp g++
iostream
stdio.h-->cstdio
3 名字空间
1)定义
namespace 名字空间名{成员...}
2)使用
名字空间名::成员;
名字空间指令,using namespace std;
名字空间声明,using 名字空间名::成员;
3)无名名字空间
4)名字空间嵌套
4 C++的结构体、联合体、枚举
5 C++的字符串:string
================================
今天
六 C++的布尔类型
1 bool类型是C++中基本类型,专门表示逻辑值:true/false
2 bool在内存上占一个字节:1表示true,0表示false
3 bool类型可以接收任意类型和表达式的结果,其值非0则为true,值为0则为false
七 操作符别名(了解)
&& --》 and
|| --》 or
{ --》 <%
} --》 %>
...
八 C++函数
1 函数重载
strcpy(char*,const char*)
strcpy(char*,const char*,int)
1)定义
在相同的作用域,定义同名的函数,但是它们的参数表必须有所区分,这样的函数构成重载关系。
注:重载和返回返回类型无关
2)函数重载匹配
调用重载关系函数函数,编译器根据实参与形参匹配程度,自动选择最优的重载版本。
当前编译器的匹配原则 g++ V4.8.1:
完全匹配>常量转换>升级转换>降级转换>省略号匹配
3)函数重载原理
C++编译器通过函数换名,将参数表的信息整合到新的函数名中,实现解决函数重载与名字冲突的矛盾
eg:
代码里面写的函数:
void func(int i,double d);
void func(int i);
编译之后函数名将改变:
func(int,double)-->_Z4funcid
func(int)-->_Z4funci
笔试题:函数声明中加入extern "C"作用?
要求C++编译器不会函数做换名,便于C程序调用该函数。
语法:
extern "C" void func(..){..}
----------------------------
extern "C"{
void func1(){}
void func2(){}
}
-------
2 函数的缺省参数(默认实参)
1)可以为函数的部分参数和全部参数指定缺省值,调用该函数,如果不给实参,就取缺省值作为相应的形参值。
eg:
void func(int a,int b,int flag=0){..}
int main(void){
func(10,20,1);
func(10,20);
}
2)缺省参数必须靠右,如果一个参数有缺省值,那么这个参数的右侧所有参数都必须带有缺省值。
3)如果函数的定义和声明分开,缺省参数应该写在函数的声明部分,而定义部分不写。
void func(){..} -->函数的定义
void func(); -->函数的声明
3 函数的哑元参数
1)定义:只有类型而没有变量名的形参称为哑元
eg:
void func(int a,int/*哑元*/){}
2)使用哑元的场景
--》为了兼容旧代码
算法库:
void math_func(int a,int b){...}
使用者:
int main(void){
math_func(10,20);
...
math_func(20,30);
}
------------------------------------
升级算法库:
void math_func(int a,int=0){...}
--》操作符重载时,区分前后++/--(后面讲)
4 内联函数(inline)
//笔试题:inline关键字的作用
1)定义
使用inline关键修饰的函数,表示这个函数是内联函数,编译器将尝试做内联优化,避免函数调用的开销,提高代码的执行的效率。
2)适用场景
--》多次调用的小而简单的函数适合内联
--》调用次数极少获取大而复杂的函数不适合内联
--》递归函数不适合内联
注:内联只是一种建议而不是强制要求,能否内联主要取决于编译器,有些函数不加inline关键字修改也会被默认处理为内联,有些函数即便加了inline关键字也会被编译器忽略。
---------------------
笔试题:
1)代码片段1
for(int i=0;i<100000;i++)
for(int j=0;j<100;j++)
...
2)代码片段2
for(int i=0;i<100;i++)
for(int j=0;j<100000;j++)
...
---------------------
九 C++的动态内存分配
//笔试题:C++中new/delete和C中malloc和free区别
1 回顾C中动态分配
1)分配:malloc()
2)释放:free()
3)错误:返回值
eg:
int* p = (int*)malloc(sizeof(int));
*p = 100;
free(p);//避免泄露
p = NULL;//避免使用野指针
2 C++使用运算符分配动态内存
1)分配:new、new[]
2)释放:delete、delete[]
3)错误处理:异常(后面讲)
eg:
//int* p = new int;//分配不初始化
//*p = 100;
int* p = new int(100);//分配同时初始化
delete p;
p = NULL;
-----------------
int* parr = new int[10];//分配数组
parr[0]=10;
parr[1]=20;
...
delete[] parr;
parr = NULL;
--------------------------
十 C++的引用(Reference)
1 定义
1)引用就是某个变量的别名,对引用的操作与对该变量的操作完全相同。
2)语法规则
类型& 引用名 = 变量名;
注:引用在定义时必须初始化,而且初始化以后不能修改引用的目标。
注:引用类型和它所绑定的目标变量类型要一致
eg:
int a = 10;
int& b = a;//b就是a的别名
b++;
cout << a << endl;//11
int c = 20;
b = c;//将c的值赋值给b(a)
cout << a << endl;//20
2 常引用
1)定义引用时加const修饰,即为常引用,不能通过常引用修改引用的目标。
const 类型& 引用名 = 变量名;
eg:
int a = 10;
const int& b = a;//b就是a的常引用
b = 200;//error
2)普通的引用只能引用左值,而常引用也叫做万能引用,既能引用左值,也能引用右值。
==================
注:关于左值和右值
1)左值:可以放在赋值运算符的左侧
-->普通变量都是左值
-->前++/--表达式结果是左值
-->赋值表达式的结果是左值
eg:
int a = 1;
++a = 20;//ok
cout << a << endl;//20
++++++a;
cout << a << endl;//23
eg:
int a = 3,b = 5;
int c = 0;
(c = a) = b;//ok
cout << c << endl;//5
2)右值:只能放在赋值运算符右侧
--》字面值常量
--》大多数表达式的值
eg:
int a = 3,b = 5;
(a + b) = 10;//error,a+b的结果是右值
--》函数返回值
eg:
int foo(void){
int a = 100;
return a;//分配临时变量=a
}
int main(void)
{
int res = foo();//函数调用结果是临时变量
cout << res << endl;//100
int& r = foo();//error
const int& cr = foo();
return 0;
}
--day03--
回顾:
1 bool类型
2 操作符别名
3 C++的函数
1)函数重载//extern "C"
2)缺省参数 void func(int b,int a = 10)
3)哑元参数 void func(int)
4)内联函数 inline
4 动态内存分配 new/delete new[]/delete[]
5 C++引用
1)引用即别名: 类型& 引用名 = 变量名;
2)常引用(万能引用)
3)关注左值和右值
===============================
今天
十 C++的引用(Reference)
1
2
3 引用型函数参数
1)将引用用于函数的参数,可以修改实参变量的值,同时也能减小函数调用的开销。
2)引用参数有可能意外修饰实参的值,如果不希望修改实参变量本身,可以将其定义为常引用,提高传参效率的同时还可以接收常量型的实参。
4 引用型函数返回值
1)可以将函数返回类型声明为引用,避免函数返回值所带来的开销。
2)一个函数返回类型被声明为引用,那么该函数返回值可以是一个左值。
3)为了避免在函数外部修改引用的目标,可以为该引用附加常属性。
eg:
int& foo(void){
static int a = 100;
return a;
}
int main(void)
{
foo() = 200;//ok
}
注:不要返回局部变量的引用,因为所引用的目标内存会在函数返回以后被释放,危险!
但是可以返回成员变量、静态变量、全局变量的引用
//笔试题:引用和指针的区别...
5 引用和指针
1)从C语言角度,引用的本质就是指针,但是在C++中推荐使用引用而不是指针。
eg:
double d = 3.14;
double& rd = d;
double* const pd = &d;
rd <=等价=>*pd
2)指针定义可以不做初始化,其目标可以修改(指针常量除外),而引用必须做初始化,而且一旦初始化所引用的目标能再改变。
eg:
int a=3,b=5;
int* p;//ok,可以不初始化
//int& r;//error
p = &a;
p = &b;
int& r = a;
r = b;//不是修改引用的目标,而是对r(a)进行赋值
//后面了解
3)可以定义指针的指针(二级指针),但是不能定义引用的指针
eg:
int a = 100;
int* p = &a;
int** pp = &p;
int& r = a;
int& * pr = &r;//error
int* pr = &r;//ok,是一个普通的指针
4)可以定义指针的引用,但是不能定义引用的引用
eg:
int a = 100;
int* p = &a;
int* & rp = p;//ok,指针的引用
-----------
int& r = a;
int&& rr = r;//error,在C++11中称为右值引用
int& r2 = r;//ok,不能称为引用的引用,只是一个普通的引用,相当于给a再起一个别名。
5)可以指针数组,但是不能引用数组
eg:
int a=1,b=2,c=3;
int* parr[3] = {&a,&b,&c};//指针数组
int& rarr[3] = {a,b,c};//error
6)可以定义数组引用
eg:
int arr[3] = {1,2,3};
//rarr称为数组引用,给数组起一个别名
int (&rarr)[3] = arr;//ok
arr[0] <-等价-> rarr[0]
7)和函数指针一样,可以定义函数引用,语法和函数指针一致。
eg:
void func(int a,int b){...}
int main(void){
//定义和使用函数指针
void (*pfunc)(int,int) = func;
pfunc(10,20);
//定义和使用函数引用
void (&rfunc)(int,int) = func;
rfunc(10,20);
}
======================================
十一 类型转换
1 隐式类型转换
eg:
char c = ‘A‘;
int n = c;//隐式类型转换
-----------
void foo(int n){..}
foo(c);//隐式类型转换
-----------
int foo(void){
char c = ‘A‘;
return c;//隐式类型转换
}
2 强制类型转换
eg:
char c = ‘A‘;
int n = (int)c;//C风格的强制转换
int n = int(c);//C++风格的强制转换
3 C++增加了四种操作符形式的显式类型转换
1)静态类型转换
语法:
目标类型变量 =
static_cast<目标类型>(源类型变量);
适用场景:
用于将void*转换为其它类型指针。
eg:
int a = 100;
void* pv = &a;//ok
int* pi = pv; //error
int* pi = static_cast<int*>(pv);//ok
2)动态类型转换(后面讲)
语法:
目标类型变量 =
dynamic_cast<目标类型>(源类型变量);
3)常类型转换
语法:
目标类型变量 =
const_cast<目标类型>(源类型变量);
适用场景:用于去除一个指针或引用的常属性
eg:
int a = 100;
const int* pa = &a;
*pa = 200;//error
int* pa2 = const_cast<int*>(pa);
*pa2 = 200;//ok
-------------------
const int& r = a;
r = 300;//error
int& r2 = const_cast<int&>(r);
r2 = 300;//ok
4)重解释类型转换
语法:
目标类型变量 =
reinterpret_cast<目标类型>(源类型变量);
适用场景:
-->任意类型的指针或引用之间的转换
-->在指针和整型数之间的转换
eg:
int addr = 0x12345678;
int* p =
reinterpret_cast<int*>(0x12345678);
*p = 100;
======================
小结:
1 慎用宏,用const、enum、inline替换
#define PAI 3.14 --》const double PAI = 3.14
#define STATE_SLEEP 0
#define STATE_RUN 1
#define STATE_STOP 2
--》enum STATE{SLEEP,RUN,STOP};
#define max(a,b) ((a)>(b)?(a):(b))
--> inline int max(int a,int b){
return a > b ? a : b;
}
2 变量随用随声明同时初始化
3 尽量使用new/delete分配,取代malloc/free
4 少用void*、指针计算、联合体、强制转换
5 尽量使用string表示字符串,少用C中的char*表示的字符串
====================================
十二 类和对象//了解
1 什么是对象?
万物皆对象,任何一种事物都可以看做是对象。
2 如何描述对象?
通过对象的属性(名词、数量词、形容词)和行为(动词)描述和表达对象。
3 面向对象的程序设计
对自然世界中对象观察引入到编程实践的一种理念和方法。这种方法称为"数据抽象",即在描述对象时把细节的东西剥离出去,只考虑一般性的,有规律性的和统一性的东西。
4 什么是类?
类是将多个对象的共性提取出来定义的一种新的数据类型,是对 对象的属性和行为的抽象描述。
练习:
复习前面内容(重点看第八、十章节)
预习 十三章 类的定义和实例化
--day04--
回顾:
1 引用型函数参数
2 引用型函数返回值
3 引用和指针
4 类型转换
1)静态类型转换:将void*--》其它类型指针
2)常类型转换:去除指针或引用的常属性
3)重解释类型转换
5 类和对象
===========================================
今天
十三 类的定义与实例化
1 类的一般形式
class/struct 类名:继承表{
访问控制限定符:
类名(形参表):初始化表{}//构造函数
~类名(void){}//析构函数
返回类型 函数名(形参表){}//成员函数
数据类型 变量名;//成员变量
};
2 访问控制限定符
1)public
公有成员,类内部和外部都可以访问的成员
2)private
私有成员,只能在类的内部访问的成员
3)protected
保护成员(后面讲)
注:class定义类,默认访问控制属性是private,而struct定义的类默认的访问控制属性是public。
eg:
class/struct A{
public:
member1;//公有成员
private:
member2;//私有成员
public:
member3;//公有成员
void func(){
member1 = 200;//ok
member2 = 100;//ok,类内部可以访问私有成员
}
};
int main(){
A a;
a.member1 //ok,类的外部可以访问公有成员
a.member2 //error,类的外部不能访问私有成员
}
3 构造函数(Constructor)
class 类名{
类名(构造形参表){
//构造函数体
}
};
1)函数名与类名相同,并且没有返回类型
2)构造函数在创建对象时自动被调用,不能像普通的成员函数一样直接去调用。
3)构造函数主要负责初始化对象,即初始化成员变量。
4 对象的创建和销毁
1)在栈区创建单个对象//重点掌握
类名 对象名(构造实参表);
类名 对象名 = 类名(构造实参表);
注:两种写法完全等价
2)在栈区创建多个对象(对象数组)//了解
类名 对象数组[元素个数] =
{类名(构造实参表),类名(构造实参表)..}
3)在堆区创建/销毁单个对象//重点掌握
创建:类名* 对象指针 = new 类名(构造实参表);
销毁:delete 对象指针;
4)在堆区创建/销毁多个对象//了解
创建:
类名* 对象指针 =
new 类名[元素个数]{类名(构造实参表),...};
销毁:
delete[] 对象指针;
===============================
练习:实现电子时钟类,让其构造接收当前系统时间,以秒为单位运行。
class Clock{
public:
Clock(time_t t){
tm* local = localtime(&t);
m_hour = local->tm_hour;
m_min = local->tm_min;
m_sec = lcoal->tm_sec;
}
void run(void){
while(1){
计时:+1秒
打印当前时间;
sleep(1);
}
}
private:
int m_hour;//小时
int m_min;//分钟
int m_sec;//秒
};
int main(){
Clock c(time(NULL));
c.run();
return 0;
}
=============
5 多文件编程
1)类的声明部分放在xx.h头文件中
2)类的实现部分放在xx.cpp源程序中
3)类的使用代码一般会放在其它文件中
练习:使用多文件编程方法重构电子时钟类
=============
十四 构造函数和初始化表
1 构造函数可以重载、可以带有缺省参数,也可以定义哑元.
2 缺省构造函数(无参构造函数)
1)如果类中没有定义任何构造函数,那么编译器会提供一个缺省(无参)构造函数。
--》对于基本类型的成员变量不做初始化
--》对类 类型的成员变量,会自动调用相应类型的无参构造函数来初始化
eg:
class B{
public:
B(void){
m_j = 0;
}
int m_j;
};
class A{
public:
int m_i;//基本类型的成员变量
B m_b;//类 类型的成员变量(成员子对象)
};
int main(void){
A a;
cout << a.m_i << endl;//不确定
cout << a.m_b.m_j << endl;//0
}
2)如果类中定义了构造函数,无论是否有参数,那么编译都不会再提供无参构造函数
3 类型转换构造函数(单参构造函数)
class 目标类型{
public:
[explicit] 目标类型(源类型 src){...}
};
可以实现源类型到目标类型的隐式转换
注:使用explicit关键字,可以强制这种转换必须显式的完成。
4 拷贝(复制)构造函数
1)用一个已定义的对象构造同类型的副本对象时,会调用该类的拷贝构造函数.
class 类名{
类名(const 类名& that){...}
};
eg:
class A{...};
A a1;
A a2(a1);//拷贝构造函数
A a2 = a1;//和上面完全等价
2)如果一个类没有定义拷贝构造函数,那么编译器会为其提供一个缺省拷贝构造函数
--》对基本类型的成员变量,按字节复制
--》对类 类型成员变量,调用相应类拷贝构造函数
注:在大部分情况下,不需要自己定义拷贝构造函数,因为编译器所提供的缺省拷贝构造函数已经很好用了。
3)拷贝构造函数的调用时机
--》用已定义的对象作为类型对象的构造函数
--》以对象形式向函数传递参数
--》从函数中返回对象(有可能会被编译器优化掉)
--day05--
回顾:
1 类的定义和实例化
1)class/struct
2)访问控制属性:public private
3)构造函数
4)对象的创建和销毁
类名 对象名(...);
类名* 对象指针名 = new 类名(...);
5)多文件编程
2 构造函数和初始化表
1)可以重载、缺省参数、哑元
2)缺省(无参)构造函数
3)类型转换构造函数(explicit)
4)拷贝构造函数
类名(const 类名& that){..}
==================================
十四 构造函数和初始化表
...
5 初始化表
1)语法形式
class 类名{
类名(形参表):成员变量1(初值),...{}
};
2)必须要使用初始化表的场景
--》如果有类 类型的成员变量,而该类又没有无参构造函数,则必须通过初始化表来初始化该成员变量。
--》类中包含"const"和“引用”成员变量,必须在初始化表中显式的初始化。
注:成员变量的初始化顺序由声明顺序决定,而与初始化表的顺序无关。
练习:使用初始化表为电子时钟类增加计时器功能。
如果使用日历时间初始化时钟对象,表现为时钟功能。
如果以无参的方式构造时钟对象,通过构造函数的初始化表将时分秒初始化为0:0:0,表现为计时器功能。
十五 this指针与常函数
1 this指针
1)类的成员函数和构造函数中都有一个隐藏的类 类型指针参数,名为this.
--》对于普通的成员函数,this指针就是指向调用该函数的对象。
--》对于构造函数,this指针指向正在被创建的对象
2)需要显式使用this的场景
--》区分作用域
--》从成员函数返回调用对象的自身(返回自引用)
--》从类的内部销毁对象自身
2 常成员函数(常函数)
1)在一个普通成员函数后面加上const 关键字,这个成员函数就称为常函数。
返回类型 函数名(形参表) const {函数体}
2)常函数中的this指针是一个常指针,不能在常函数中修改成员变量的值。
注:被mutable关键字修饰的成员变量可以在常函数中被修改。
3)非 常对象既可以调用常函数也可以调用非 常函数,但是常对象只能调用常函数,不能调用非 常函数。
4)函数名和形参表相同的成员函数,其常版本和非常版本可以构成重载关系,常对象调用常版本,非常对象调用非常版本。
-------------
十六 析构函数(Destructor)
1 语法形式
class 类名{
public:
~类名(void){//清理对象在创建分配的动态资源}
};
1)函数名必须是"~类名"
2)没有返回类型,也没有参数,不能被重载。
3)主要负责清理对象在创建分配的动态资源。
2 当对象被销毁时,析构函数将自动被执行
1)栈对象当其离开作用域时,其析构函数被作用域终止的右花括号"{"调用。
2)堆对象的析构函数被delete运算符调用。
3 如果一个类没有显式定义析构函数,那么编译器会为该类提供一个缺省的析构函数;
-->对基本类型的成员变量什么也不做
-->对类类型的成员变量,会调用相应的析构函数
4 对象的创建和销毁过程
1)对象的创建
--》分配内存
--》构造成员子对象
--》执行构造函数代码
2)对象的销毁
--》执行析构函数代码
--》析构成员子对象
--》释放内存
================================
十七 拷贝构造和拷贝赋值
1 浅拷贝和深拷贝
1)如果一个类中包含指针形式的成员变量,缺省的拷贝构造函数只是复制了指针变量的本身,而没有复制指针所指向的内容,这种拷贝方式称为浅拷贝。
2)浅拷贝将导致不同对象之间的数据共享,如果数据存放在堆区,可能会在析构时引发"double free"异常,因此就需要自己定义一个支持复制指针指向的内容的拷贝构造函数,即深拷贝。
练习:自定实现String类,支持深拷贝构造
class String{
public:
//构造函数
String(const char* str = ""):
m_str(
strcpy(new char[strlen(str)+1],str)){
/*
m_str = new char[strlen(str)+1];
strcpy(m_str,str);
*/
}
//析构函数
~String(void){
delete[] m_str;
m_str = NULL;
}
//深拷贝构造
//提供访问接口函数
const char* c_str(void)const{
return m_str;
}
private:
char* m_str;
};
int main(void)
{
String s1("Hello");
String s2 = s1;//拷贝构造
cout << s1.c_str() << endl;//Hello
cout << s2.c_str() << endl;//Hello
return 0;
}
--day06--
回顾:
1 构造函数初始化表
类名(形参表):初始化表{函数体}
2 this指针
1)区分作用域
2)返回自引用
3)自销毁
3 常成员函数//mutable
返回类型 函数名(形参表)const{函数}
4 析构函数
~类名(void){清理对象创建时分配的动态资源}
5 对象的创建和销毁
创建:
-》分配内存
-》构造成员子对象
-》执行构造函数代码
销毁:
-》执行析构函数代码
-》析构成员子对象
-》释放内存
===============================
十七 拷贝构造和拷贝赋值
1 浅拷贝和深拷贝
1)如果一个类中包含指针形式的成员变量,缺省的拷贝构造函数只是复制了指针变量的本身,而没有复制指针所指向的内容,这种拷贝方式称为浅拷贝。
2)浅拷贝将导致不同对象之间的数据共享,如果数据存放在堆区,可能会在析构时引发"double free"异常,因此就需要自己定义一个支持复制指针指向的内容的拷贝构造函数,即深拷贝。
2 在C++类中会提供一个缺省的拷贝赋值运算符函数,完成两个对象直接的赋值操作。但是它和缺省拷贝构造函数类似,也是浅拷贝,为了得到深拷贝赋值的效果,必须自己定义拷贝赋值运算符函数
s2 = s3;//s2.operator=(s3)
类名& operator=(const 类名& that){
if(this != &that){//1)防止自赋值
2)释放旧资源
3)分配新资源
4)拷贝新数据
}
return *this;//5)返回自引用
}
十八 静态成员(static)
1 静态成员变量
class 类名{
static 数据类型 变量名;//声明静态成员变量
};
数据类型 类名::变量名 = 初值;//定义和初始化
1)静态成员变量不属于对象,但是可以通过对象去访问它。
2)不能在构造函数中定义和初始化,需要在类的外部单独的定义和初始化
3)静态成员变量内存在全局区;
4)静态成员变量可以通过类名直接访问
类名::静态成员变量
5)静态成员变量在类所创建多个对象之间是共享的
2 静态成员函数
class 类名{
static 返回类型 函数名(形参表){函数体}
};
1)静态成员函数没有this指针,也没有const属性
2)可通过"类名::"直接访问,也可以通过对象访问
注:静态成员函数只能访问静态成员
3 单例模式:一个类只允许创建唯一的对象
1)禁止在类的外部创建对象:私有化构造函数
2)类的内部维护唯一对象:静态成员变量
3)提供访问单例对象的方法:静态成员函数
4)创建方式:
--》饿汉式:单例对象无论用或不用,程序启动即创建。
--》懒汉式:单例对象在用的时候再创建,不用即销毁。
eg:
class A{
public:
static A& get(void){
return m_a;
}
private:
A(void);
A(const A&);
static A m_a;
};
===================
十九 成员指针(了解)
1 成员变量指针
1)定义
类型 类名::*成员指针变量名 = &类名::成员变量;
2)使用
对象.*成员指针变量名;
对象指针->*成员指针变量名;
".*":成员指针解引用运算符
"->*":间接成员指针解引用运算符
注:成员变量指针的本质是类中特定成员在对象中的相对地址。
2 成员函数指针
1)定义
返回类型 (*类名::成员函数指针)(形参表)
= &类名::成员函数名;
2)使用
(对象.*成员函数指针)(实参表);
(对象指针->*成员函数指针)(实参表);
==================
二十 操作符重载
eg:复数x+yi 3+4i
(1+2i) + (3+4i) = 4+6i
1 双目操作符
1.1 运算类的双目操作符(L # R):+ - * /
-->左右操作数可以是左值也可以是右值
-->表达式结果是右值
1)成员函数形式
L#R的表达式会被编译器自动处理为L.operator#(R)的成员函数调用,该函数的返回值即为表达式的值。
2)全局函数形式
1.2 赋值类的双目操作符
练习:复习String类和单例模式实现
--day07--
回顾:
1 拷贝构造和拷贝赋值(String)
1)深拷贝和浅拷贝
2)拷贝赋值操作符函数
2 静态成员
1)静态成员变量(全局区)
2)静态成语函数(没有this指针)
3)静态成员函数只能访问静态成员
3 单例模式
1)私有化构造函数,禁止在类的外部创建对象
2)静态成员变量,在类内部维护一个唯一对象
3)静态成员函数获取单例对象
4)创建方式:饿汉式/懒汉式
4 成员指针(了解)
=======================
今天
二十 操作符重载
eg:复数x+yi 3+4i
(1+2i) + (3+4i) = 4+6i
1 双目操作符(L # R)
1.1 运算类的双目操作符:+ - * /
-->左右操作数可以是左值也可以是右值
-->表达式结果是右值
1)成员函数形式
L#R的表达式会被编译器自动处理为L.operator#(R)的成员函数调用,该函数的返回值即为表达式的值。
2)全局函数形式
L#R的表达式会被编译器自动处理为operator#(L,R)的全局函数调用,该函数的返回值即为表达式的值。
注:通过friend关键字,可以把一个全局函数声明为某个类的友元,友元函数可以访问类中的任何成员。
1.2 赋值类的双目操作符
-->左操作数是左值(不能是常量),右操作数可以是左值也可以是右值
-->表达式结果是左值,就是左操作的自身
1)成员函数形式:
L # R --》L.operator#(R);
2)全局函数形式:
L # R --》operator#(L,R);
--------------------
2 单目操作符重载 #O
2.1 计算类单目操作符:-(取负) ~ !
-->操作数可以左值也可以值右值
-->表达式结果是一个右值
1)成员函数形式
#O --》O.operator#();
2)全局函数形式
#O --》operator#(O);
2.2 自增减单目操作符: ++/--
1)前缀自增减
--》操作数是左值
--》表达式结果就是操作数自身,也是左值
成员函数形式:
#O--> O.operator#()
全局函数形式:
#O--> operator#(O)
2)后缀自增减
--》操作数是左值
--》表达式结果是自增减之前的副本(右值)
成员函数形式:
O#--> O.operator#(int/*哑元*/)
全局函数形式:
O#--> operator#(O,int/*哑元*/)
---------------------------
3 插入和提取操作符: << >>
功能:实现自定义类型的输入和输出的功能
注:只能用全局函数形式
#include <iostream>
ostream cout;
istream cin;
cout << a;//operator<<(cout,a);
friend ostream& operator<<(
ostream& os,const RIGHT& right){...}
cin >> a;//operator>>(cin,a);
friend istream& operator>>(
istream& is,RIGHT& right);
==========================================
练习:实现3x3矩阵类,支持如下操作符重载
<< + - += -= -(取负) ++(前后) --(前后)
要求:除了<<,都使用成员函数形式
class M33{
public:
M33(void){
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
m_a[i][j] = 0;
}
M33(int a[][3]){
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
m_a[i][j] = a[i][j];
}
const M33 operator+(const M33& m)const{
int a[3][3]={0};
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
a[i][j]= m_a[i][j] + m.m_a[i][j];
M33 res(a);
return res;
}
private:
int m_a[3][3];
};
int main(void)
{
int a1[3][3] = {1,2,3,4,5,6,7,8,9};
int a2[3][3] = {9,8,7,6,5,4,3,2,1};
M33 m1(a1);
M33 m2(a2);
}
m1 + m2
1 2 3 9 8 7 10 10 10
4 5 6 + 6 5 4 = 10 10 10
7 8 9 3 2 1 10 10 10
-------
m1 - m2
1 2 3 9 8 7 -8 -6 -4
4 5 6 - 6 5 4 = 2 0 2
7 8 9 3 2 1 4 6 8
4 new/delete 操作符
static void* operator new(size_t size){...}
static void operator delete(void* p){...}
5 函数操作符"()"
6 下标操作符"[]"
--day08--
二十 操作符重载
5 函数操作符"()"
功能:让对象当做函数来使用
注:对参数的个数、返回类型没有限制
eg:
class A{...};
A a;
//a.operator()(100,200)
a(100,200);
6 下标操作符"[]"
功能:让对象可以当做数组一样去使用
注:非常对象返回左值,常对象返回右值
eg:
string s("hello");
//s.operator[](0)
s[0] = ‘H‘;//ok
cout << s << endl;//Hello
-------------
const string s("hello");
s[0] = ‘H‘;//error
7 类型转换操作符
功能:实现自定义类型的转换
class 源类型{
operator 目标类型(void)const{...}
};
8 操作符重载的限制
1)不是所有的操作符都能重载,下面几个不能重载
--》作用域限定操作符 "::"
--》直接成员访问操作符 "."
--》直接成员指针解引用操作符 ".*"
--》条件操作符 "?:"
--》字节长度操作符 "sizeof"//获取类型大小
--》类型信息操作符 "typeid"//获取类型信息(后面讲)
2)如果一个操作符的所有操作数都是基本类型,则无法重载。
int operator+(int a,int b){//error
return a - b;
}
3)操作符重载不会改变编译器预定义优先级
4)操作符重载无法改变操作数的个数(函数操作符例外)
5)无法通过操作符重载机制发明新的的操作符
[email protected]()//error
6)只能使用成员形式不能使用全局函数形式的操作符
=、()、[]、->
===================
二十一 继承(Inheritance)
1 继承的概念
通过一种机制表达类与类之间共性和特性的方式,利用已有的类定义新的类,这种机制就是继承。
eg:描述人类、学生类、教师类
人类:姓名、年龄、吃饭、睡觉
学生类:姓名、年龄、吃饭、睡觉、学号、学习
教师类:姓名、年龄、吃饭、睡觉、工资、讲课
-------------
人类:姓名、年龄、吃饭、睡觉
学生类继承人类:学号、学习
教师类继承人类:工资、讲课
人类(父类/基类)
/ \
学生类 教师类(子类/派生类)
2 继承的语法
1)定义
class 子类名
:继承方式1 基类1,继承方式2 基类2,...{};
2)继承方式
public:公有继承
protected:保护继承
private:私有继承
eg:
class A{...};
//A类派生B类,B类继承A类
//A类称为基类(父类),B称为子类
class B:public A{
//B类中会存在一份A类中的成员
};
3 公有继承特性(Public)
3.1 子类对象可以继承基类的属性和行为,通过子类访问基类中的成员,如果是基类对象在访问它们一样。
注:子类对象中包含基类的部分称为"基类子对象"
3.2 向上和向下造型
1)向上造型//重点掌握
将子类类型的指针或引用转换为基类类型的指针或引用。这种操作范围缩小的类型转换,在编译器看来是安全的,所以可以隐式转换。
eg:
class Base{};
class Derived:public Base{};
void func(Base& b){}
int main(void){
Derived d;
Base* pb = &d;//向上造型
Base& rb = d;//向上造型
func(d);//向上造型
}
2)向下造型//了解
将基类类型指针或引用转换为子类类型的指针或引用。这种操作范围放大的类型转换,在编译器看来是危险的,因此必须显式转换。
3.3 子类继承的基类成员
1)在子类中,可以直接访问基类中公有成员和保护成员,就如同它们是子类自己的成员一样。
2)在子类中,所继承过来的私有成员虽然存在(占据内存),但是不可见,所以无法直接访问,但是可以提供保护或公有的接口函数来间接访问。
3)基类的构造函数和析构函数,子类无法继承,但是可以在子类自己的构造函数中通过初始化表,显式的指明基类部分(基类子对象)的初始化方式。
eg:
class Base{
public:
Base(int data):m_data(data){}
int m_data;
};
class Derived:public Base{
public:
Derived(int data1,int data2)
:Base(data1),m_data2(data2){}
int m_data2;
};
3.4 子类隐藏基类中的成员
1)子类和基类中定义同名的成员函数,因为作用域不同,不会构成重载关系,而是一种隐藏关系。如果需要在子类中访问所隐藏的基类成员,可以使用作用域限定操作符来显式指明。
2)通过using声明可以将基类的成员函数引入子类的作用域的,形成重载。//不推荐
4 继承方式和访问控制属性
1)三种访问控制限定符:影响访问该类成员的位置
访问控制 访问控制 内部 子类 外部 友元
限定符 属性 访问 访问 访问 访问
public 公有成员 ok ok ok ok
protected 保护成员 ok ok no ok
private 私有成员 ok no no ok
---------------------------------------------
2)三种继承方式:影响通过子类访问基类中的成员的可访问性。
---------------------------------------------
基类中的 在公有子 在保护子 在私有子
类中变成 类中变成 类中变成
公有成员 公有成员 保护成员 私有成员
保护成员 保护成员 保护成员 私有成员
私有成员 私有成员 私有成员 私有成员
----------------------------------------------
注:私有子类和保护子类的指针或引用,不能转换为其基类类型的指针或引用(不能向上造型).
练习:复习公有继承的语法特性
--day09--
回顾:
1 操作符重载:() [] 类型转换操作符
2 操作符限制
-------------------
3 继承
1)概念
2)语法
class 子类:继承方式 基类{}
基类方式:public、protected、private
3)公有继承的特性
--》可以把一个子类对象看做是基类对象
--》向上造型:子类指针/引用--》基类指针/引用
--》向下造型:基类指针/引用--》子类指针/引用
--》子类继承基类的成员:公有、保护、私有
--》子类隐藏基类的成员:解决--》"类名::"
4)继承方式和访问控制属性
=============================
今天:
二十一 继承(Inheritance)
...
5 子类的构造函数和析构函数
5.1 子类的构造函数
1)如果子类构造函数没有显式指明基类子对象的初始化方式,那么该子对象将以无参方式被初始化。
2)如果希望基类子对象以有参的方式被初始化,必须在子类构造函数的初始化表中显式指明。
class 子类:public 基类{
子类(...):基类(基类子对象构造实参表){}
};
3)子类对象的构造过程
--》分配内存
--》构造基类子对象(按继承表顺序)
--》构造成员子对象(按声明顺序)
--》执行子类的构造代码
5.2 子类的析构函数
1)子类的析构函数,会自动调用基类的析构函数,析构基类子对象。
2)子类对象的销毁过程
--》执行子类的析构代码
--》析构成员子对象(按声明逆序)
--》析构基类子对象(按继承表逆序)
--》释放内存
3)基类析构函数不能调用子类的析构函数,对一个指向子类对象的基类指针使用delete运算符,实际被执行的仅是基类的析构函数,所释放的仅是基类子对象构造时的分配的动态资源,而子类特有的动态资源将会形成内存泄露。
eg:
class A{
A(void){动态资源分配}
~A(void){动态资源销毁}
};
class B:class A{
B(void){动态资源分配}
~B(void){动态资源销毁}
};
A* pa = new B;//pa:指向子类对象的基类指针
delete pa;//子类析构函数执行不到,内存泄露
解决:明天讲..
-------------------------
6 子类的拷贝构造和拷贝赋值
6.1 子类的拷贝构造
1)子类没有定义拷贝构造函数,编译器会为子类提供缺省拷贝构造函数,该函数会自动调用基类的拷贝构造函数,初始化基类子对象。
2)子类定义拷贝构造函数,需要使用初始化表,显式指明基类子对象也以拷贝方式进行初始化。
6.2 子类的拷贝赋值
1)子类没有定义拷贝赋值操作符函数,编译器会提供缺省拷贝赋值函数,该函数会自动调用基类的拷贝赋值函数,复制基类子对象。
2)子类定义拷贝赋值操作符函数,需要显式调用基类的拷贝赋值函数,完成对基类子对象的复制。
7 多重继承
1)一个子类继承多个基类,这样继承方式称为多重继承。
eg:
技术员 经理
\ /
技术主管
eg:
电话 播放器 计算机
\ | /
智能手机
2)向上造型时,编译器会根据各个基类子对象在子类对象中内存布局,进行适当的偏移计算,保证指针的类型和其所指向的目标对象类型一致。
3)名字冲突问题
一个子类的多个基类存在相同的名字,当通过子类访问这些名字时,编译器会报歧义错误--名字冲突。
解决名字冲突的通用做法就是显式地通过作用域限定,指明所访问的名字属于哪一个基类。
如果产生冲突的名字是成员函数,并且参数不同,也可以通过using声明,让其在子类中形成重载,通过函数重载匹配解决冲突问题。
8 钻石继承
1)一个子类的多个基类源自共同的祖先基类,这样继承结构称为钻石继承。
A(m_data)
/ \
B C
\ /
D
2)公共基类(A)子对象,在汇聚子类(D)对象中,存在多个实例。在汇聚子类中,或者通过汇聚子类对象,去访问公共基类的成员,会因为继承路径不同而导致结果不一致。
3)通过虚继承可以让公共基类(A)子对象,在汇聚子类(D)对象中的实例唯一,并且为所有子类共享,这样即使沿着不同的继承路径去访问公有基类的成员,结果也是一致的。
9 虚继承的语法
A(m_data)
/ \
B C(virtual)
\ /
D(负责构造虚基类子对象)
1)在继承表中使用virutal关键字
2)由汇聚子类的构造函数负责构造虚基类子对象
3)公共基类的所有子类,都必须在构造函数的初始化表中显式指明其初始化方式,否则编译器将会选择以无参方式初始化。
----------------------------
练习:薪资计算
员工
/ | \
技术员 经理 销售员
\ / \ /
技术主管 销售主管
所有员工:姓名、工号、职位等级、出勤率
经理:绩效奖金(元/月)
技术员:研发津贴(元/小时)
销售员:提成比率(x%)
薪资=基本工资+绩效工资
基本工资计算=职位等级的固定额度*出勤率(输入);
绩效工资根据具体的职位而定:
普通员工:基本工资一半
经理:绩效奖金*绩效因数(输入)
技术员:研发津贴*工作小时数*进度因数(输入)
销售员:提成比率*销售额度(输入)
技术主管:(技术员绩效工资+经理绩效工资)/2
销售主管:(销售员绩效工资+经理绩效工资)/2
结果:打印员工信息,输入必要数,计算和打印工资
--day10--
回顾:
1 子类的构造函数和析构函数
2 子类对象创建和销毁过程
创建:分配内存->构造基类子对象->构造成员子对象->执行子类的构造函数代码
销毁:执行子类的析构函数代码->析构成员子对象->
基类子对象->释放内存
3 子类的拷贝构造和拷贝赋值
4 多重继承
1)向上造型//自动偏移
2)名字冲突问题
5 钻石继承
A
/ \
B C
\ /
D
解决:虚继承
1)在继承表中使用virtual
2)末端的汇聚子类负责构造公共基类子对象
=============================================
今天:
eg:
图形(位置/绘制)
/ \
矩形(长和宽/绘制) 圆形(半径/绘制)
二十二 多态(Polymorphic)
1 函数重写(虚函数覆盖)、多态概念
如果将基类中的某个成员函数声明为虚函数,那么子类与其具有相同原型的成员函数就也将是虚函数,并且对基类中的版本形成覆盖。
这时,通过指向子类对象的基类指针,或者引用子类对象的基类引用,调用该虚函数,实际被执行的将是子类中覆盖版本,而不是基类中的原始版本,这种语法现象称为多态.
eg:
class Base{
public:
virtual void foo(void){
cout << "Base::foo" << endl;
}
};
class Derived:public Base{
public:
void foo(void){
cout << "Derived::foo" << endl;
}
};
int main(void){
Derived d;
Base* pb=&d;//pb指向子类对象的基类指针
Base& rb=d;//rb引用子类对象的基类引用
pb->foo();//Derived::foo
rb->foo();//Derived::foo
}
2 函数重写要求(虚函数覆盖条件)
1)类中普通的成员函数可以声明为虚函数,而全局函数、类中的静态成员函数、构造函数都不能声明为虚函数。
注:析构函数可以为虚函数(后面讲)
2)只有在基类中以virtual关键字修饰的函数,才能作为虚函数被子类覆盖,而与子类中virtual关键字无关。
3)虚函数在子类中的覆盖版本和改函数在基类中原始版本要拥有相同的函数签名,即函数名、形参表、常属性必须严格一致.
4)如果基类中的虚函数返回基本类型的数据,那么子类的覆盖版本必须返回相同的类型。
5)如果基类的虚函数返回类类型指针(A*)或引用(A&),那么允许子类返回其子类的指针(B*)或引用(B&)。--类型协变
class A{};
class B:public A{};
3 多态的条件
1)在满足虚函数覆盖前提下,必须要通过指针或引用调用该虚函数,才能表现出来。
2)调用虚函数的指针也可以是this指针,只要它是一个指针子类对象的基类指针,调用虚函数时,同样可以表现多态的特性。//重点掌握
4 纯虚函数、抽象类和纯抽象类
1)纯虚函数
virtual 返回类型 函数名(形参表)[const]=0;
2)抽象类
如果一个类中包含纯虚函数,那么这个类就是抽象类,抽象类不能创建对象。
3)纯抽象类(有名接口类)
如果一个抽象类除了构造函数和析构函数以外的所有成员函数都是纯虚函数,那么该类就是纯抽象类。
注:如果子类没有覆盖抽象基类的纯虚函数,那么该子类就也是一个抽象类,类的抽象属性可以被继承.
eg:PDF文档阅读器
5 多态实现原理(了解)
通过虚函数表和动态绑定,参考poly.jpg
1)动态绑定会增加内存开销
2)虚函数调用会增加时间开销
3)虚函数不能被内联优化
结论:如果没有多态的语法要求,最好不要使用虚函数。
6 虚析构函数
1)基类析构函数不能调用子类的析构函数,对一个指向子类对象的基类指针使用delete运算符,实际被执行的仅是基类的析构函数,所释放的仅是基类子对象构造时的分配的动态资源,而子类特有的动态资源将会形成内存泄露。
2)将基类的析构函数声明为虚函数,那么子类的析构函数就也是一个虚函数,并且可以对基类的虚析构函数形成有效的覆盖,可以表现多态的特性。
3)这时delete一个指向子类对象的基类指针,实际被调用的将是子类的析构函数,而子类的析构函数在执行后又会自动调用基类的析构函数,避免内存泄露。
//笔试题:虚析构函数的作用?
=========================================
练习:薪资计算
员工
/ | \
技术员 经理 销售员
\ / \ /
技术主管 销售主管
所有员工:姓名、工号、职位等级、出勤率
经理:绩效奖金(元/月)
技术员:研发津贴(元/小时)
销售员:提成比率(x%)
薪资=基本工资+绩效工资
基本工资计算=职位等级的固定额度*出勤率(输入);
绩效工资根据具体的职位而定:
普通员工:基本工资一半
经理:绩效奖金*绩效因数(输入)
技术员:研发津贴*工作小时数*进度因数(输入)
销售员:提成比率*销售额度(输入)
技术主管:(技术员绩效工资+经理绩效工资)/2
销售主管:(销售员绩效工资+经理绩效工资)/2
结果:打印员工信息,输入必要数,计算和打印工资
class 员工{
double 计算工资(){
return 基本工资() + 绩效工资();
}
virutal void 绩效工资(){}
};
class 技术员:public 员工{
void 绩效工资(){}
}
int main(){
员工对象.计算工资();
技术员对象.计算工资();
}
--day11--
1 多态概念
2 虚函数覆盖
3 多态条件(this)
4 纯虚函数、抽象类、纯抽象类
5 多态实现原理
6 虚析构函数
-----------------------
二十三 运行时的类型信息
1 typeid运算符
#include <typeinfo>
typeid(类型/对象)
1)返回typeinfo的对象,用于描述类型信息。
2)在typeinfo类中包含了一个name()成员函数返回字符串形式类型信息。
3)typeinfo类支持"=="、"!="的操作符重载,可以直接进行类型之间的比较,如果类型之间存在多态的继承关系,还可以利用多态的特性确定实际的对象类型。
eg:
cout << typeid(int).name() << endl;//i
2 动态类型转换dynamic_cast
语法:
目标类型变量 =
dynamic_cast<目标类型>(源类型变量);
场景:用于具有多态特性的父子类指针和引用之间进行显示的转换(向下造型)。
注:在转换过程中,会检查目标对象和期望转换的对象类型是否一致,如果一致转换成功,不一致转换失败。
如果转换的是指针,返回NULL表示失败,如果转换的是引用抛出异常"bad_cast"表示失败。
二十四 异常(Exception)
1 常见的错误
1)语法错误
2)逻辑错误
3)功能错误
4)设计缺陷
5)需求不符
6)环境异常
7)操作不当
2 传统C中错误处理机制
1)通过返回值表示错误
优点:函数调用路径中所有的栈对象,都能正确的被析构,不会内存泄露
缺点:错误处理流程比较复杂,逐层判断,代码臃肿
2)通过远跳机制处理错误
优点:不需要逐层判断,一步到位错误处理,代码精炼
缺点:函数调用路径中的栈对象失去被析构的机会,有内存泄露的风险
3 C++异常机制
结合两种传统错误处理的优点,同时避免它们的缺点,在形式上实现一步到位的错误处理,同时保证所有栈对象能正确的被析构。
4 异常语法
1)异常抛出
throw 异常对象;
eg:
throw -1;
throw "File Error";
class FileError{};
throw FileError(...);
2)异常捕获
try{
可能发生异常的语句;
}
catch(异常类型1){
针对异常类型1的处理
}
catch(异常类型2){
针对异常类型2的处理
}
...
catch(...){
针对其它异常的处理
}
注:catch子句根据异常对象类型自上而下顺序匹配,因此对类类型的异常捕获要写到对基类类型的异常捕获的前面,否则子类的异常将被提前截获。
5 函数异常说明
1)可以任何函数中增加异常说明,说明该函数所可能抛出的异常类型。
返回类型 函数名(形参表) throw(异常类型表){}
2)函数的异常说明是一种承诺,表示该函数所抛出的异常不会超出说明的范围。如果函数抛出了异常说明以外的异常,则无法正常捕获,导致进程终止。
3)异常说明极端形式
--》不写异常说明,表示可以抛出任何异常
--》空异常说明,throw(),表示不会抛出任何异常
4)如果函数声明和定义分开,要保证异常说明的类型一致。
补充:函数重写要求
如果基类中的虚函数带有异常说明,那么该函数在子类中覆盖版本不能说明比基类抛出更多的异常,否则将因为"放松throw限定"而编译报错.
6 标准异常类
class exception{
public:
exception()throw(){}
virtual ~exception()throw(){}
virtual const char* what() const throw();
};
eg:
class A:public exception{
public:
const char* what() const throw(){
//...
return "Error A";
}
};
try{
throw A();
}
catch(exception& ex){
ex.what();//Error A
}
7 构造函数和析构函数中的异常
1)构造函数抛出异常,该对象将会被不完整构造,这样对象的析构函数永远不会被自动执行。因此在构造函数抛出异常之前,需要手动销毁之前分配的动态资源。
2)析构函数最好不要抛出异常
---------------------------------
二十五 I/O流 //了解
1 主要的I/O流类
ios
/ \
istream ostream
/ | \ / | \
istrstream ifstream iostream ofstream ostrstream
/ \
strstream fstream
2 格式化I/O
1)格式化函数
eg:
cout << 10/3.0 << endl;//3.33333
cout.precision(10);
cout << 10/3.0 << endl;//3.333333333
2)流控制符
eg:
cout << 10/3.0 << endl;//3.33333
cout << setprecision(10) <<
10/3.0 << endl;//3.333333333
3 字符串流
#include <strtstream>//过时
istrstream ostrstream strstream
#include <sstream>//当前用的比较多
istringstream//读取内存,sscanf()
ostringstream//写入内存,sprintf()
stringstream //读写内存
4 文件流
#include <fstream>
ifstream //读取文件,fscanf
ofstream //写入文件,fprintf
fstream //读写文件
5 二进制I/O //fread、fwrite
ostream& ostream::write(
const char* buffer,size_t num);
istream& istream::read(
char* buffer,streamsize num);