您的位置:首页 > 编程语言 > C语言/C++

个人C++学习笔记

2020-04-07 12:31 2041 查看

笔记内容来自本人学习 狄泰软件学院 唐佐林 老师的视频,相关课件截图已授权

本文章未经许可不允许转载和复制保存

C++第三课

C语言中的const

1.const修饰的变量是只读的,本质还是变量
2.const修饰的局部变量在栈上分配空间,所以可以用指针修改该空间的值而修改该const局部变量的值
3.const修饰的全局变量在只读存储区分配空间,修改值的话程序会崩溃
4.const只在编译期有用,在运行期无用

PS:C语言中定义真正意义上的常量,只能用enum枚举来定义,const不行

C++中的const

1.当碰见const声明(不是const引用)时在符号表中放入常量(符号表是编译器内部的数据结构)
2.编译过程中若发现使用常量则直接以符号表中的值替换
3.编译过程中若发现下述情况则给对应的常量分配储存空间:(为了兼容C语言)
(1)该const常量为全局,且对const常量使用了extern
(2)对const常量使用&操作符

注意:C++编译器虽然可能为const常量分配空间,但不会使用其存储空间中的值

示例程序:

#include <stdio.h>

int main()
{
const int c = 0;
int* p = (int*)&c;

*p = 5;

printf("c = %d\n",c);//打印0,使用的是符号表里的值

printf("*p = %d\n",*p);//打印5,使用的是c对应的内存空间的值

return 0;
}

C++第五课

1.引用是一个变量的别名,即是一段内存空间的代号
2.如果要使一个已经存在的非只读变量拥有只读属性,只需要定义一个新的const变量做为它的引用,则该新的变量为只读的
3.当使用字面常量对const引用进行初始化时,c++编译器会为常量值分配空间,并将引用名作为这段内存空间的别名,即该const引用会用该段内存,取值的时候不会去符号表拿而是去该段内存拿
4.当用一个引用对另一个引用初始化时,新的引用跟另一个引用所代表的是同一段内存空间的别名,取地址时结果相同

示例程序:
(1)

int a = 0;

const int& b = a;

int* p = (int*)&b;

b = 5;//Error,b是只读变量

*p = 5;//Ok,修改变量a的值

(2)

const int& b = 1;

int* p = (int*)&b;

b = 5;//Error,只读变量

*p = 5;//Ok,修改变量b所对应的内存空间的值

引用有没有自己的内存空间呢?(有)

示例程序:
(1)

#include <stdio.h>

struct TRef
{
char& r;
};

int main()
{
char c = 'c';
char& rc = c;
TRef ref = {c};

printf("sizeof(char&) = %d\n",sizeof(char&));  //1
printf("sizeof(rc) = %d\n",sizeof(rc));  //1

printf("sizeof(TRef) = %d\n",sizeof(TRef));  //4
printf("sizeof(ref.r) = %d\n",sizeof(ref.r));  //1

return 0;
}

引用的本质:指针,所以第三个打印的是4(因为它还没有初始化为是谁的引用,所以打印出来的是指针的大小)

引用在C++中的内部实现是一个指针常量
Type& name; -> Type* const name;

C++第六课

1.内联函数是为了替换宏代码块,起到类型检查等功能
2.内联函数声明和定义时都要加inline
3.调用内联函数时C++编译器直接将函数体插入函数调用的地方
4.内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)
5.C++编译器不一定满足函数的内联请求
6.不能对内联函数进行取址

其他资料:

C++第七课

1.给函数参数默认值的时候,如果声明和定义分开,那声明和定义时给的默认参数值必须一致
2.参数的默认值必须从右向左提供,因为从右向左入栈
3.C++中可以为函数提供占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数
4.在C++中,void func(void);等价于void func();在C语言中不等价
5.默认参数和占位参数的出现是为了兼容C语言中的不规范写法

C++第八课

函数重载条件

1.参数个数不同
2.参数顺序不同
3.参数类型不同
符合其中一个条件即可重载函数

编译器调用重载函数的准则:

1.将所有同名函数作为候选者
2.尝试寻找可行的候选函数
(1)精确匹配实参
(2)通过默认参数能匹配实参
(3)通过默认类型转换匹配实参
3.函数返回值类型不能做为函数重载的依据

匹配失败:
1.最终寻找到的候选函数不唯一,则出现二义性,编译失败。比如:

int func(int a,int b,int c=0)
{
return a*b*c;
}

int func(int a,int b)
{
return a+b;
}

调用func(1,2)时出现二义性,编译失败。

2.无法匹配所有候选者,函数未定义,编译失败。

PS:重载函数之间是相互独立的函数,取地址时地址不一样

C++第九课

函数重载遇上函数指针:
-将重载函数名赋值给函数指针时:

1.根据重载规则挑选与函数指针参数列表一致的候选者
2.严格匹配候选者的函数类型与函数指针的函数类型,即函数的返回值类型也要匹配

比如:

int func(int a)
{
return a;
}

int func(int a,int b)
{
return a+b;
}

void func(int a)
{
std::cout<<a<<std::endl;
}

typedef int(*PFUNC)(int a)

当调用PFUNC p = func;时,匹配的是第一个函数

注意:

1.函数重载必然发生在同一个作用域中
2.编译器需要用参数列表或函数返回值类型进行函数选择
3.无法直接通过函数名得到重载函数的入口地址

正确做法为:

printf(“%p\n”,int(*)(int)func);
printf(“%p\n”,int(*)(int,int)func);
printf(“%p\n”,void(*)(int)func);

C++和C相互调用

1.实际工程中C++和C代码相互调用是不可避免的
2.C++编译器能够兼容C语言的编译方式
3.C++编译器会优先使用C++编译的方式
4.extern关键字(在C++里才有)能强制让C++编译器进行C方式的编译

extern “C”
{
//code...
}

比如:
在add.h文件中:

int add(int a,int b);

在add.c文件中:

int add(int a,int b)
{
return a+b;
}

以上两个文件都是C语言的,当我们在C++文件中要引用add.h头文件时,如在main.cpp中,

#include <stdio.h>

extern “C”
{
#include “add.h”
}

int main()
{
int c = add(1,2);
printf(“c = %d\n”,c);

return 0;
}

这样才编译通过,否则编译器指出函数未定义。因为add函数在add.c中定义,而不是add.cpp中定义

如何保证C的代码一定以C编译器的方式编译呢?
答:_cplusplus宏是C++编译器里的一个标准宏,所有C++编译器都有,利用这个特性,可以这样做:

#ifdef _cplusplus
extern “C”
{
#endif
//C code...

#ifdef _cplusplus
}
#endif

注意事项

1.C++编译器不能以C的方式编译重载函数
2.编译方式决定函数名被编译后的目标名
3.C++编译方式将函数名和参数列表编译成目标名
4.C编译方式只将函数名做为目标名编译

C++第十课

new关键字与malloc函数的区别:

1.new关键字是C++的一部分
2.malloc是有C库提供的函数
3.new以具体类型为单位进行内存分配
4.malloc以字节为单位进行内存分配
5.new在申请单个类型变量时可进行初始化
6.malloc不具备内存初始化的特性
7.相应的,delete会触发类对象的析构函数,free不会

C++中的命名空间:

1.在C语言中只有一个全局作用域,所有的全局标识符共享同一个作用域,标识符之间可能发生冲突
2.在C++中提出了命名空间的概念,命名空间将全局作用域分成不同的部分,不同命名空间中的标识符可以同名而不会发生冲突,命名空间可以相互嵌套
3.全局作用域也叫默认命名空间
4.在多个文件总定义同名命名空间时,如果内容不同,则追加,如果内容相同,则链接错误

使用方法:

1.使用整个命名空间:using namespace name;
2.使用命名空间中的变量:using name::variable;
3.使用默认命名空间中的变量:::variable

C++第十一课

1.static_cast:

(1)用于基本类型间的转换
(2)不能用于基本类型指针间的转换
(3)用于有继承关系类对象之间的转换和类指针之间的转换

2.const_cast

(1)用于去除变量的只读属性
(2)强制转换的目标类型必须是指针或引用

3.reinterpret_cast

(1)用于指针类型间的强制转换
(2)用于整数和指针类型间的强制转换

4.dynamic_cast

(1)用于有继承关系的类指针间的转换
(2)用于有交叉关系的类指针间的转换
(3)具有类型检查的功能
(4)需要虚函数的支持
(5)转换不成功则返回空指针

C++第十二课

const什么时候为只读变量?什么时候是常量?

1.只有用字面量初始化的const常量才会进入符号表
2.使用其它变量初始化的const常量仍然是只读变量
3.被volatile修饰的const常量不会进入符号表

PS:在编译期间不能直接确定初始值的const标识符,都被作为只读变量处理
注意:const引用的类型与初始化变量的类型相同时,初始化变量称为只读变量,不同时生成一个新的只读变量,即使初始化变量的值变了,被初始化的引用因生成了一个新的只读变量而不会随之改变,如:

char c = ‘c’;
char& rc = c;
const int& trc = c;

rc = ‘a’;

printf(“c = %c\n”,c); //输出a
printf(“rc = %c\n”,rc);//输出a
printf(“trc = %c\n”,trc);//输出c,因char跟int不同类型,trc是一个独立于c的只读变量

关于引用和指针的疑问

1.指针是一个变量
(1)值为一个内存地址,不需要初始化,可以保存不同的地址
(2)通过指针可以访问对应内存地址中的值
(3)指针可以被const修饰成为常量或者只读变量
2.引用只是一个变量的新名字
(1)对引用的操作(赋值,取地址等)都会传递到代表的变量上
(2)const引用使其代表的变量具有只读属性
(3)引用必须在定义时初始化,之后无法代表其他变量。因为在编译器内部,使用指针常量来实现引用,所以必须在定义时初始化

引用要注意的问题
示例程序:

#include <stdio.h>

int a = 1;

struct SV
{
int& x;
int& y;
int& z;
};

int main()
{
int b = 2;
int* pc = new int(3);
SV sv = {a,b,*pc};//成功
int& array[] = {a,b,*pc};//Error

return 0;
}

Error的原因是,站在编译器的角度,因C++要兼容C语言,而在C语言中,*一个数组的地址是一片连续的内存空间,而a,b,pc的内存地址并不连续,所以编译失败。

C++第十九课

特殊的构造函数

-无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且函数体为空
-拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制

所以,下列定义的类并不是空的,因为有隐藏的默认提供的无参构造函数和拷贝构造函数
class Test{};

初始化和赋值
在C++中,初始化和赋值不一样,初始化会触发构造函数,而赋值不会

拷贝构造函数

1.格式:claas_name(const class_name& another){}
2.意义:兼容C语言的初始化方式

拷贝构造函数中的深拷贝和浅拷贝

1.浅拷贝:拷贝后对象的物理状态相同
2.深拷贝:拷贝后对象的逻辑状态相同

PS:编译器提供的拷贝构造函数只进行浅拷贝

什么时候需要进行深拷贝

1.对象中有成员指代了系统中的资源
2.成员指向了动态内存空间
3.成员打开了外存中的文件
4.成员使用了系统中的端口
5…

示例程序:

class Array
{
private:
int* m_data;
int m_length;
public:
Array()
{
data = new int[5];
}

Array(const Array& another)
{
/*
* 浅拷贝:
* this->m_data = another.m_data;
* this->length = another.length;
*
* 深拷贝:
*
* this->m_data = new int[5];
* this->length = another.length;
*/
}

~Array()
{
delete[] this->m_data;
}
};

不进行深拷贝的话,析构的时候会发生重复释放同一片内存空间,会段错误

C++第二十课

初始化成员列表

1.初始化成员列表在构造函数的函数体之前
2.初始化成员列表的初始化顺序跟写代码的顺序无关,只跟成员的声明顺序有关

类中的const成员

1.类中的const成员的初始化只能在构造函数的初始化列表中进行初始化
2.类中的const成员会被分配空间
3.类中的const成员的本质是只读变量

说明:
1.编译器无法直接得到const成员的初始值,因此无法进入符号表成为真正意义上的常量。
2.构造函数的函数体在执行之前,对象已经被创建好了,构造函数的函数体只不过是为了初始化对象的各种成员

示例程序:

class School
{
private:
const int ID;
const string name;

public:
//执行的顺序是先给ID初始化,再给name初始化,因为ID先声明,name后声明
School():name("GDUT"),ID(66666)
{

}

void setID(int id)
{
int& p = const_cast<int&>(ID);

p = id;
}

int getID()const
{
return this->ID;
}
};

int main()
{
School s;

printf("s.ID = %d\n",s.getID());//66666

s.setID(99999);

printf("s.ID = %d\n",s.getID());//99999,说明s.ID是只读变量而不是真正的常量

return 0;
}

C++第二十一课

对象的构造顺序

1.局部对象的构造顺序依赖于程序的执行流,即main函数自上而下的顺序
2.堆对象的构造顺序依赖于new的使用顺序
3.全局对象的构造顺序是不确定的。所以,在程序当中尽量不使用全局变量,且全局变量之间不要有依赖性

C++第二十四课

单个对象的构造顺序

1.调用父类的构造过程
2.调用成员变量的构造函数(调用顺序与声明顺序相同)
3.调用类自身的构造函数

类中的const问题

1.const对象本质只是只读变量,而不是常量,可用const_cast改变const属性
2.const对象只能调用类中的const成员函数
3.const成员函数中不能修改成员变量的值

关于类成员的疑问

1.每个具体的类对象都有自己独立的成员变量
2.所有同类的类对象共享一套成员函数,因为函数是在代码区的,是只读的

C++第二十五课

类中静态成员变量

1.静态成员变量属于整个类所有
2.静态成员变量的生命周期不依赖于任何对象,程序结束其生命周期才结束
3.可以直接通过类名::直接访问公有的静态成员变量
4.所有对象共享类的静态成员变量
5.可以通过对象名访问公有静态成员变量
6.静态成员变量的内存不属于具体的某一个对象,当sizeof(一个具体对象)时,不包括静态成员变量的大小 静态成员变量特性

1.在定义时用static修饰
2.静态成员变量需要在类外单独分配空间
3.静态成员变量在程序内部位于全局数据区
4.静态成员变量要在类外初始化

静态成员变量初始化语法规则:
Type ClassName::VarName = value;

注意
类中static const int、static const short、static const char类型的成员变量可以在声明的同时进行初始化,这是特例!
参考资料:http://www.jswku.com/p-11143731.html

C++第二十六课

静态成员函数

1.静态成员函数属于整个类所有
2.可以通过类名::直接访问公有静态成员函数
3.可以通过对象名访问公有静态成员函数

示例程序

class Test
{
public:
static int Func1();//只声明,在类外实现
static void Func2(){}
}
int Test::Func1(){  return 0;   }

注意

1.静态成员函数没有隐藏的this指针,即在函数体内部不能用this
2.静态成员函数只能直接访问静态成员变量和静态成员函数,不能访问普通成员变量和普通成员函数
3.类中所有的成员函数都被隐式声明为inline函数

C++第二十七课

二阶构造模式

1.在构造函数调用之前类对象已经诞生,构造函数只是起到初始化的作用,不影响类对象的诞生
2.构造函数中可以执行return语句,构造函数立即结束
3.构造函数不能保证初始化逻辑一定成功,即不一定成功返回一个完整的对象

解决方案
进行二阶构造
步骤:

1.进行资源无关的初始化操作,即不可能出现异常情况的操作
2.进行需要使用系统资源的操作,即可能出现异常情况,如内存申请,访问文件等

将以上步骤分开完成。
示例程序:

#include <iostream>

using namespace std;

class Array
{
private:
int m_length;
int* m_pointer;

Array(int length)
{
this->m_length = length;
}

bool construct()
{
bool ret = true;

m_pointer = new int[m_length];

if( m_pointer!=NULL )
{
for(int i=0;i<m_length;i++)
m_pointer[i] = 0;
}
else
ret = false;

return ret;
}

public:
static Array* NewInstance(int length)
{
Array* ret = new Array(length);

if(!(ret&&ret->construct()))
{
delete ret;
ret = NULL;
}

return ret;
}

};

int main()
{
Array* array = Array::NewInstance(10);

return 0;
}

分析

1.在Array类中,构造函数设为私有,目的是不允许直接通过类名定义类对象。
2.构造函数完成简单的赋值操作,这里不可能出现异常操作;而同样设为私有的construct函数则完成需要申请内存的操作,这里可能会出现申请内存失败而出现异常。
3.通过public函数中的static函数Newinstance来创建对象的时候,通过判断construct函数的返回值来决定是否返回该对象,如果内存没有申请成功,则删除对象返回NULL,否则返回该完整对象。

C++第二十八课

友元

1.友元是单向的,不能传递
2.在类中以friend关键字声明友元
3.类的友元可以是其它类或者具体函数
4.友元不是类的一部分
5.友元不受类中访问级别的限制
6.友元可以直接访问具体类的所有成员

友元的尴尬

1.C++是面向对象的,类应该有封装性,即应该有自己的私有成员,不应该被外界直接访问,而友元破坏了这一特性
2.友元是为了兼顾C语言的高效而诞生的
3.在实际开发中不应该使用友元

注意事项

1.类的友元可以是其它类的成员函数
2.类的友元可以是某个完整的类

示例程序:

class B;
class A
{
private:
void funcA();

public:
friend class B;//声明整个类B为类A的友元

void func()
{
funcA();
}
};

class B
{
private:
static char c;
public:
friend void A::funcA();//只声明类A里的funcA函数为类B的友元
};
char B::c = 'c';

inline void A::funcA()
{
std::cout<<B::c<<std::endl;
}

注意
funcA函数在最后的地方定义。声明和定义分开,是为了避免编译时报找不到B的错误,如果直接在类A中声明并定义funcA是通过不了编译的。此写法等同于.h文件和cpp文件分开写。
请回顾inline的知识点。

C++第三十课

操作符重载

1.全局的操作符重载函数的参数需要提供该操作符对应数量的参数
2.类中的操作符重载函数少一个参数,少左操作数,可以通过this来完成
3.编译器优先在成员函数中寻找操作符重载函数

C++第三十五课

函数对象

1.要在且只能在类的成员函数里重载()操作符
2.函数调用操作符可以定义不同参数的多个重载函数
3.函数对象用于在工程中取代函数指针

示例程序:

class Func
{
public:
void operator()()
{
std::cout<<"Hey~"<<endl;
}
};

int main()
{
Func f;
f();
return 0;
}

C++第三十六课

1.类中默认提供的=操作符重载函数只实现浅拷贝
2.赋值操作符的重载的返回值类型必须是引用,因为要连续赋值
3.赋值操作符的重载函数的参数的类型必须是const className& obj
4.重载赋值操作符的时候要避免自己给自己赋值,可通过this!=&obj判断
5.当要进行二阶构造时,要把构造函数和拷贝构造函数设为private,但是允许用=
6.一个空类里有默认无参构造函数、默认拷贝构造函数、默认=重载函数、默认析构函数
7.在一个代码工程里,不要混合C++和C语言

C++第三十八课

逻辑操作符的重载

1.不建议进行逻辑操作符的重载,因为短路规则会失效。因为此时重载了逻辑操作符,其变成了函数性质,不再只是操作符,且回归到函数参数的入栈顺序,重载后的逻辑操作符的执行顺序不一定从左往右
2.在工程当中应尽量用其他方法代替逻辑操作符重载
3.如果非要进行逻辑操作符的重载,则最好在全局函数中重载,左移和右移操作符也是。如果在成员函数中重载,调用的时候默认左操作数是本类对象,则调用的时候是obj<<cout,这样违反习惯

C++第三十九课

逗号表达式的重载

1.重载后的逗号表达式的效果不变,指的是逗号表达式的值是最后一个表达式的值。但是此时逗号表达式里的语句不一定从左往右进行执行
2.工程中不要重载逗号表达式

C++第四十课

前置和后置操作符的重载

1.两者均可在全局或者成员函数中进行重载
2.重载前置操作符无需参数
3.重载后置操作符需要一个int类型的占位符以示区分

示例程序:

Test& operator ++()//前置++
{
++mValue;
return *this;
}
Test operator ++(int)//后置++
{
Test ret(mValue);
mValue++;
return ret;
}

前置++返回的是引用,因为可以连续多个++,如int a = 0; a++++++;而后置++不能。
前置++和后置++的汇编代码完全一样,当讨论它们的区别的时候要分两种情况:

1.对于普通类型,它们的效率几乎相同
2.对于类对象,因为后置++要生成一个临时对象ret,所以效率比前置++低

类中new和delete操作符的重载

1、在类中重载new和delete,尽管不必显式地使用static,但实际上仍在创建static成员函数。
2、当编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator
new()而不是全局版本的new()。但如果要创建这个类的一个对象数组时,全局的operator
new()就会被立即调用,用来为这个数组分配内存。当然可以通过为这个类重载运算符的数组版本,即operator new[]和operator
delete[]来控制对象数组的内存分配。 3、使用继承时,重载了的类的new和delete不能自动继承使用

C++第四十一课

转换构造函数

再论构造函数

1.构造函数可以定义为不同类型的参数
2.参数满足下列条件时称为转换构造函数
(1)有且仅有一个参数
(2)参数是基本类型
(3)参数是其它类类型

示例程序:

class Test
{
private:
int mValue;
public:
Test(int i)
{
this->mValue = i;
}
};

int main()
{
int i;

Test t = i;

return 0;
}

编译器看Test类里有单个参数的构造函数Test(int i);且其参数类型为int,跟main函数里的i匹配,所以Test t = i;等价为Test t = Test(i);

注意
这种编译器隐式的类型转换会让程序以意想不到的方式进行工作,是工程中bug的重要来源,所以要杜绝这种隐式的类型转换!

解决方法
通过explicit关键字杜绝编译器的转换尝试。转换构造函数被explicit修饰时只能进行显示转换,如通过static_cast(value);

示例程序:

class Test
{
private:
int mValue;
public:
explicit Test(int i)
{
this->mValue = i;
}
};

int main()
{
int i;

Test t = i;  //error

return 0;
}

C++第四十二课

类型转换函数

1.C++类中可以定义类型转换函数
2.类型转换函数用于将本类对象转换为其它类型

格式

operator Type()
{
Type ret;
//...
return ret;
}

示例程序:

class Test
{
private:
int mValue;
public:
explicit Test(int i)
{
this->mValue = i;
}

operator int()
{
return mValue;
}
};
int main()
{
int i = Test(100);

cout<<i<<endl;  //输出100

return 0;
}

注意

1.类型转换函数与转换构造函数具有同等的地位
2.编译器能够隐式的使用类型转换函数

示例程序:

class Test;
class Value
{
public:
Value(Test& t){}
};

class Test
{
private:
int mValue;
public:
Test(){}

operator Value(){}
};

int main()
{
Test t();
Value v = t;

return 0;
}

本程序代码编译不通过。
原因
在Value类中有针对于Test类的转换构造函数,而Test类中又存在针对于Value类的类型转换函数。因为这两种函数具有同等地位,所以编译器会犯难,不知道要选择哪一个

小结

1.无法抑制隐式的类型转换函数调用
2.类型转换函数可能与转换构造函数冲突
3.工程中以Type toType()的公有成员函数代替类型转换函数,如string类的toInt()、toDouble()等成员函数

C++第四十四课

protected与private

1.类中用private修饰的成员和成员函数,在本类的外部,包括子类中,都不能直接被访问;而用protected修饰的成员和成员函数,不能在类的外部访问,但在子类中可以直接访问
2.protected是专门为继承而设计的,没有protected就没有办法完成代码复用

C++第四十五课

不同的继承方式

示例程序:

class Parent
{
private:
int a;
protected:
int b;
public:
int c;
Parent(){}
};

class Child_A:protected Parent
{
private:
int ma;
public:
Child_A()
{
ma = b;
}
};

class Child_B:private Parent
{
private:
int mb;
public:
Child_B()
{
mb = b;//可以访问父类的b
}
};

int main()
{
Child_A ca;
Child_B cb;

cout<<ca.c<<endl;  //error

return 0;
}

以private继承为例,并不是说父类原本所有属性的成员都对子类来说都是private不能直接访问,而是父类的所有属性的成员归属到了子类的private属性下面,在子类中依旧可以直接访问父类原本的public、protected成员和成员函数,只不过在新建子类对象的时候,不能在外部直接访问子类继承过来的父类的public成员及成员函数了,因为它们现在都有了子类的private属性

注意

1.在工程中只需要使用public继承即可
2.protected和private继承带来的复杂性远大于实用性

C++第四十六课

继承中的构造与析构

1.构造顺序:父类构造->本类的成员构造->自身构造
2.当本类的构造函数的初始化列表或函数体中没有显示的调用父类和本类成员的构造函数时,编译器会默认调用父类和本类成员的默认无参构造函数或者有初始值的构造函数。如果父类或成员对象类中只提供了有参数的且没有初始值的构造函数时,编译器无法默认调用它们的构造函数,如果在本类的构造函数中不显示调用,则会编译出错
3.析构顺序跟构造顺序相反.

C++第四十七课

父子间的冲突

1.父类里的内容和子类里的内容属于不同的作用域,类似于不同命名空间之间的关系
2.所以,子类可以有跟父类完全一样的成员和成员函数
3.当子类里有跟父类一样的成员的时候,父类的成员会被隐藏,通过子类调用该名字成员的时候调用的是子类自己的那个成员而不是父类的
4.不过可以在子类里通过子类.父类名::同名同类型成员变量名来访问父类的那个相同成员
5.如果子类有跟父类完全一样的成员函数,此时不是构成重载,因为重载的条件是在同一作用域中才算。同相同成员函数一样,当通过子类来调用该相同成员函数的时候,调用的是子类里的而不是父类的
6.即使子类中有跟父类对象重名的成员变量,子类的内存大小也包括父类中重名的成员变量,包括父类中的虚函数指针(如果父类中有虚函数的话)
7.单继承的情况下,子类自己有新的虚函数,那么会修改父类的那个vptr,生成新的虚函数表,不会独立再创建一个vptr指针,即只有一个vptr
8.多继承的时候,比如继承两个父类,两个父类都有虚函数,那么子类里就会生成两个vptr

示例程序:

class Parent
{
protected:
int pa;

public:
Parent()
{
this->pa = 100;
}
void print()
{
std::cout<<"Parent::print()"<<std::endl;
}
};

class Child
{
private:
int pa;
public:
Child()
{
this->pa = 200;
}

void print()
{
std::cout<<"Child::print()"<<std::endl;
}

int getPa()
{
return this->pa;
}
};

int main()
{
Child c;

cout<<c.getPa()<<endl;  //200

c.print();   //Child::print()

return 0;
}

C++第四十八课

同名覆盖引发的问题
父子间的赋值兼容
Part 1

1.子类对象可以直接赋值给父类对象
2.子类对象可以直接初始化父类对象
3.父类指针可以直接指向子类对象
4.父类引用可以直接引用子类对象

Part 2
当使用父类指针或引用指向子类对象时

1.子类对象退化为父类对象
2.只能访问父类中定义的成员
3.可以直接访问被子类覆盖的同名成员

PS:Part 1和Part 2即相当于直接使用一个父类对象或指针或引用

重载重写重定义

1.重载必须发生在同一作用域中的函数重载
2.重写跟重定义一个意思,发生在继承关系中,子类可以重写(重定义)父类的同名且参数列表相同的成员函数,即不在同一作用域中,相当于同名覆盖

函数重写遇上赋值兼容
示例程序:

class Parent
{
protected:
int pa;

public:
Parent()
{
this->pa = 100;
}
void print()
{
std::cout<<"Parent::print()"<<std::endl;
}
};

class Child:public Parent
{
private:
int pa;
public:
Child()
{
this->pa = 200;
}

void print()
{
std::cout<<"Child::print()"<<std::endl;
}
};

void how_to_print(Parent* p)
{
p->print();
}

int main()
{
Child c;

how_to_print(&c);  //Parent::print()

return 0;
}

问题分析

1.编译期间,编译器只能根据指针的类型判断所指向的对象
2.根据赋值兼容,编译器认为父类指针指向的是父类对象
3.因此,编译结果只可能是调用父类中定义的同名函数

C++第四十九课

多态解决同名覆盖引发的问题

多态的意义
父类指针(引用)指向

1.父类对象则调用父类中定义的函数
2.子类对象则调用子类中定义的重写函数

实现方法

1.在父类的成员函数前加virtual关键字即可,成为虚函数
2.如果子类自己还有子类,那么子类的该重写函数前也要加virtual关键字才能实现多态
3.如果子类没有自己的子类,那么子类的该重写函数前可加可不加virtual
4.函数重写必须实现多态,否则没有意义

示例程序:

class Parent
{
protected:
int pa;

public:
Parent()
{
this->pa = 100;
}
virtual void print()
{
std::cout<<"Parent::print()"<<std::endl;
}
};

class Child:public Parent
{
private:
int pa;
public:
Child()
{
this->pa = 200;
}

void print()
{
std::cout<<"Child::print()"<<std::endl;
}
};

void how_to_print(Parent* p)
{
p->print();
}

int main()
{
Child c;

how_to_print(&c);  //Child::print()

return 0;
}

静态联编与动态联编

1.静态联编是在程序的编译期间就能确定具体的函数调用,如:函数重载
2.动态联编是在程序实际运行后才能确定具体的函数调用,如:函数重写

C++第五十课

对象模型分析

class与struct

1.唯一区别是,class里的内容默认是private,而struct默认是public
2.class是一种特殊的struct
(1)在内存中class依旧可以看作变量的集合
(2)class与struct遵循相同的内存对齐规则
(3)class中的成员函数与成员变量是分开存放的,每个对象有独立的成员变量,所有对象共享类中的成员函数

class Part 1

1.运行时的对象退化为结构体的形式
2.所有成员变量在内存中依次排布
3.所有成员变量可能存在内存间隙
4.可以通过内存地址直接访问成员变量
5.访问权限关键字在运行时失效

示例程序:

class Test
{
private:
int a;

public:
Test()
{
this->a = 100;
}

int getA()
{
return this->a;
}
};

struct sTest
{
int a;
};

int main()
{
Test t;

sTest* st = reinterpret_cast<sTest*>(&t);

st->a = 200;

std::cout<<st->a<<std::endl;   //200,private失效

return 0;
}

Test类的private成员a在运行时失效,被强制转换后可以通过st在外部直接访问,并被修改为200

class Part 2

1.类的成员函数位于代码段中,sizeof(一个类名或者一个类对象)不包括其成员函数的大小,只包括其成员变量的大小
2.调用成员函数时对象地址做为参数隐式传递
3.成员函数通过对象地址访问成员变量
4.C++语法规则隐藏了对象地址的传递过程

C++第五十一课

继承对象模型

多态对象模型



示例程序:

class Parent
{
public:
int pa;
int pb;

Parent()
{
pa = 10;
pb = 20;
}

virtual print(){}
};

struct P
{
int* p;
int a;
int b;
};

int main()
{
Parent pa;

cout<<sizeof(Pa)<<endl;  //12

P* p = reinterpret_cast<P*>(&pa);

p->a = 100;
p->b = 200;

cout<<"pa.pa = "<<pa.pa<<endl;  //100
cout<<"pa.pb = "<<pa.pb<<endl;  //200

return 0;
}

分析

1.sizeof(Pa)输出12可以看出,有虚函数的类,其内部会隐藏一个vptr指针,且其内存归属于每个类对象
2.结构体P能够准确用p->a和p->b进行给pa对象的成员进行赋值,说明Parent类和结构体P的内存布局相同
3.从第二点和结构体P的内存结构可以看出,vptr指针在类的内存布局中的最前面四个字节

C++第五十二课

抽象类和接口

抽象类

1.类中只要存在一个纯虚函数,则该类则成为抽象类
2.纯虚函数的格式为virtual Type Func(参数列表)=0;
3.在抽象类中只需要像第二点那样声明纯虚函数,不用实现它
4.纯虚函数实质上还是虚函数,还是可以发生多态
5.不能定义抽象类的对象,但是可以定义抽象类的指针对象,用来发生多态
6.继承于抽象类的子类必须实现该抽象类的所有纯虚函数,如果没有全部实现,则该子类也是抽象类,只能被继承,不能定义具体对象,但也是可以定义指针对象

示例程序:

class Shape
{
protected:
string name;
public:
virtual double area()=0;
};

class Rec:public Shape
{
public:
Rec()
{
name = "Rec";
}

double area()
{
return 6.6;
}
};

double getArea(Shape* s)
{
return s->area();
}

int main()
{
Rec r;

cout<<getArea(&r)<<endl;  //6.6

return 0;
}

getArea函数的参数中可以定义Shape抽象类的指针,但是不能定义具体对象,在函数体里可以调用s->area()是因为此时发生多态,继承自Shape的子类肯定实现了Shape类里的area纯虚函数

纯虚析构函数
在虚析构函数的基础上使得基类成为抽象类。所以主要有如下两个作用:

1.删除对象时,所有子类都能进行动态识别
2.使得基类成为抽象类

注意

1.由于最终会调用到基类的析构函数,所以即使基类的析构函数为纯虚的也要给出析构函数的实现,否则产生链接错误。
2.当基类的析构函数为纯虚析构函数时,派生类既是不实现析构函数也是可以实例化的,这是因为编译器会为其提供默认的析构函数。

接口


示例程序:

class People
{
public:
void run()=0;
void shout()=0;
void write()=0;
void sleep()=0;
};

C++第五十三课
被遗弃的多重继承(上)

多重继承带来的问题(一)

示例程序:

class BaseA
{
public:
int ma;

BaseA(int a){this->ma = a;}
};

class BaseB
{
public:
int mb;

BaseB(int b){this->mb = b;}
};

class Derived:public BaseA,public BaseB
{
public:
int mc;

Derived(int a,int b,int c):BaseA(a),BaseB(b)
{
this->mc = c;
}
};

int main()
{
Derived d(1,2,3);

BaseA* pa = &d;
BaseB* pb = &d;

void* vp1 = pa;
void* vp2 = pb;

if(vp1 == vp2)
cout<<"vp1 == vp2"<<endl;
else
cout<<"vp1 != vp2"<<endl;

return 0;
}

输出结果:vp1 != vp2

由此带来的隐患分析
我们在工程中通常通过判断两个地址相不相等来判断那两个指针是否指向了同一个对象,而因多继承而引发的如上问题使这个判断方法存在了隐患

注意
上述代码中,if里不能直接用pa == pb,这样会编译错误,因为pa和pb指向的类型不同,不能直接比较

多重继承带来的问题(二)

示例程序:

class People
{
private:
string name;
string age;
public:
People(string n,string a)
{
this->name = n;
this->age = a;
}

virtual void print()
{
cout<<"name = "<<name<<"  age = "<<age<<endl;
}
};

class Teacher:virtual public People
{
public:
Teacher(string n,string a):People(n,a)
{
}
};

class Student:virtual public People
{
public:
Student(string n,string a):People(n,a)
{
}
};

class Doctor:public Teacher,public Student
{
public:
Doctor(string n,string a):Teacher(n+"123",a),Student(n+"456",a),People(n+"789",a)
{
}
};

int main()
{
Doctor d("Guapi","21");
d.print();  //Guapi789,21

d.Teacher::print();  //Guapi789,21
d.Student::print();  //Guapi789,21

return 0;
}

在虚继承的条件下,顶层父类People只由最底层的Doctor子类来构造,中间类Teacher和Student不再参与父类People的构造,因此在Doctor类里只有一个print函数,即只来自于顶层父类People。如果不虚继承的话,Doctor类里有两个print函数,分别来自Teacher类和Student类,编译d.print()的时候编译器会报错,它不知道d的print函数该用哪一个

注意
在Doctor类的构造函数的初始化列表里,必须要显示构造顶层父类People,因为此时People类只能由Doctor类来构造

C++第五十四课

被遗弃的多重继承(下)

多重继承可能产生多个虚函数表

示例程序:

class BaseA
{
public:
virtual print()
{
cout<<"BaseA::print()..."<<endl;
}
};

class BaseB
{
public:
virtual print()
{
cout<<"BaseB::print()..."<<endl;
}
};

class Derived:public BaseA,public BaseB
{

};

int main()
{
Derived d;

cout<<sizeof(d)<<endl;  //8,有两个vptr指针,即两个虚函数表

BaseA* pa = &d;
BaseB* pb = &d;
BaseB* pbb = (BaseB*)pa;

BaseB* pb3 = dynamic_cast<BaseB*>(pa);

cout<<"pb == pbb:"<<(pb == pbb)<<endl;  //0

cout<<"pb3 == pb:"<<(pb3 == pb)<<endl;  //1

return 0;
}

第一次指针比较的结果为0,原因是使用了C语言暴力式的类型转换,而第一次类型转换使用了dynamic_cast,这个需要虚函数的支持,编译器在转换pa为BaseB*的时候,自动做了指针运算,这才是正确的

工程中正确使用多继承的方法


示例程序:

class Calculation
{
private:
double mi;
public:
Calculation(double d=0)
{
this->mi = d;
}

double& getMi(){return this->mi;}

bool equal(Calculation* c)
{
return (this == c);
}
};

class interface1
{
public:
virtual void add(double d)=0;
virtual void subtract(double d)=0;
};

class interface2
{
public:
virtual void multiply(double d)=0;
virtual void divide(double d)=0;
};

class FinalCal:public Calculation,public interface1,public interface2
{
public:
void add(double d)
{
this->getMi() += d;
}

void subtract(double d)
{
this->getMi() -= d;
}

void multiply(double d)
{
this->getMi() *= d;
}

void divide(double d)
{
this->getMi() =((d!=0)?(this->getMi() / d):0);
}
};

int main()
{
FinalCal f;

Calculation* c1 = &f;

f.add(100);

f.subtract(34);

cout<<"f.mi = "<<f.getMi()<<endl;  //66

cout<<"c1 == &f:    "<<c1->equal(dynamic_cast<Calculation*>(&f))<<endl;   //1

return 0;
}

注意equal函数的妙用,dynamic_cast在这个过程中的作用

C++第五十五课

经典问题解析四

new malloc delete 和free


注意
malloc得到的不是一个合法的对象,因为没有触发构造函数;free后可能会造成内存泄漏,因为没有触发析构函数

关于虚函数


即当构造函数或析构函数中调用了类中的虚函数时,是不会发生多态的,只会调用当前类中的版本。注意区别,不是说析构函数不能定为虚函数,而是不能发生多态

关于继承中的强制类型转换

·dynamic_cast是与继承相关的类型转换关键字
·dynamic_cast要求相关的类中必须有虚函数
·用于有直接或间接继承关系的指针或引用之间
-指针:
·转换成功:得到目标类型的指针
·转换失败:得到NULL
-引用:
·转换成功:得到目标类型的引用
·转换失败:得到一个异常操作信息

注意:类型转换的结果只可能在运行阶段才能得到

C++第五十六课

函数模板的概念和意义



C++第五十七课

深入理解函数模板

函数模板深入理解

----编译器从函数模板通过具体类型产生不同的函数
----编译器会对函数模板进行两次编译 ·对模板代码本身进行一次编译 ·对参数替换后的代码进行编译,即第二次编译。此时可能会因参数类型的不同而产生不同的具体函数,它们之间是独立的,各有各的地址。在第二次编译的过程中会检查该参数类型本身的类型结构支不支持模板函数本身的逻辑

示例程序:

class Test
{
private:
Test(const Test&);
public:
Test(){}
};

template <typename T>
void Swap(T& a,T& b)
{
T t = a;
a = b;
b = t;
}

typedef void(funcI)(int&,int&);
typedef void(funcD)(double&,double&);
typedef void(funcT)(Test&,Test&);

int main()
{
funcI* fi = Swap;  //编译器自动类型推导
funcD* fd = Swap;  //编译器自动类型推导

funcT* ft = Swap;  //①编译错误,说明对模板函数二次编译

cout<<"fi = "<<reinterpret_cast<void*>(fi)<<endl;  //0x402904
cout<<"fd = "<<reinterpret_cast<void*>(fd)<<endl;  //0x4028e0

return 0;
}

上述代码①处编译错误,说明编译器会对模板函数进行二次编译,且二次编译的时候会检查该类型支不支持模板函数体里的逻辑。Test类里的拷贝构造函数是私有的,所以不支持模板函数体里的T t = a;
打印出来的两个地址不相同,说明二次编译得到的是不同的函数,在用Swap给fi和fd赋值的时候,编译器会根据fi和fd的类型进行自动类型推导

注意

----函数模板本身不允许隐式类型转换
·自动推导类型时,必须严格匹配
·显示类型指定时,能够进行隐式类型转换

对于多参数函数模板
----无法自动推导返回值类型
----可以从左向右部分指定类型参数
PS:工程中将返回值类型参数作为第一个类型参数!

示例程序:

template <typename T1,typename T2,typename T3>
T1 add(T2 t2,T3 t3)
{
return t2+t3;
}

int main()
{
//T1 = int,T2 = double,T3 = double
int r1 = add<int>(0.6,0.06);

//T1 = int,T2 = float,T3 = double
int r2 = add<int,float>(0.06,0.6);

//T1 = float,T2 = float,T3 = float
float r3 = add<float,float,float>(0.6,0.06);

cout<<"r1 = "<<r1<<endl;  //0
cout<<"r2 = "<<r2<<endl;  //0
cout<<"r3 = "<<r3<<endl;  //0.66

return 0;
}

函数模板可以像普通函数一样被重载

----C++编译器优先考虑普通函数
----如果函数模板可以产生一个更好的匹配,那么选择模板
----可以通过空模板实参列表限定编译器只匹配模板

int r1 = Max(1,2);
double r2 = Max<>(0.5,0.8);

课外补充
问题:当基类是模板类的时候,通过子类来访问基类的成员时,为什么一定要加上this->呢?

解答:内容来自知乎:https://www.zhihu.com/question/31797003?sort=created
关键点在于:不加 this,编译器就会把 f 当作一个非成员函数,因此才报的错。你没看错,是「非」成员函数。
这事得从模板的「二段式名字查找」(Two-Phase Name Lookup)说起。
根据 C++ 标准,对模板代码中的名字的查找,分为两个阶段进行:
1.模板定义阶段:刚被定义时,只有模板中独立的名字(可以理解为和模板参数无关的名字)参加查找
2.模板实例化阶段:实例化模板代码时,非独立的名字才参加查找。
有了这个背景知识就可以来分析这段代码了:

template<typename T>
class A
{
public:
void f() { std::cout << "A::f()" << std::endl; }
};

template<typename T>
class B: public A<T>
{
public:
void g() {
f();
}
};

如果没有用模板,事情会简单很多。然而这里的 B 本身是模板,需要进行二段式名字查找。
首先进入 B 的模板定义阶段,此时 B 的基类 A 依赖于模板参数 T,所以是一个「非独立」的名字。所以在这个阶段,对于 B 来说 A 这个名字是不存在的,于是 A::f() 也不存在。但此时这段代码仍旧是合法的,因为此时编译器可以认为 f 是一个非成员函数。
当稍晚些时候进入 B 的模板实例化阶段时,编译器已经坚持认为 f 是非成员函数,纵使此时已经可以查到A::f(),编译器也不会去这么做。「查非成员函数为什么要去基类里面查呢?」于是就找不到了。
那我们回过头来看 this->f():
模板定义阶段:尽管没法查到 A::f(),但明晃晃的 this-> 告诉编译器,f 是一个成员函数,不是在 B类里,就是在 B 类的基类里,于是编译器记住了。
模板实例化阶段:此时编译器查找的对象是一个「成员函数」,首先在 B 中查,没有找到;然后在其基类里查,于是成功找到 A::f(),功德圆满。

C++第五十八课

类模板的概念和意义




C++第五十九课


示例程序:

template<typename T1,typename T2>
class Test
{
public:
void add(T1 a,T2 b)
{
cout<<"void add(T1 a,T2 b)"<<endl;
cout<<a+b<<endl;
}
};

//部分特化

template<typename T>
class Test<T,T>
{
public:
void add(T a,T b)
{
cout<<"void add(T a,T b)"<<endl;
cout<<a+b<<endl;
}

void print()   //可另外添加成员函数
{
cout<<"template<typename T>"<<endl;
}
};

//部分特化
template<typename T>
class Test<T*,T*>
{
public:
void add(T* a,T* b)
{
cout<<"void add(T* a,T* b)"<<endl;
cout<<*a+*b<<endl;
}
};

//部分特化
template <typename T1,typename T2>
class Test<T1*,T2*>
{
public:
void add(T1* a,T2* b)
{
cout<<"void add(T1* a,T2* b)"<<endl;
}
};

//完全特化
template<>
class Test<void*,void*>
{
public:
void add(void* a,void* b)
{
cout<<"void add(void* a,void* b)"<<endl;
}
};

int main()
{
Test<int,double> t1;
Test<int,int> t2;
Test<void*,void*> t3;

int a = 100;
double b = 99.0;

Test<int*,double*> t4;

t1.add(6,0.66);

t2.add(6,6);
t2.print();

t3.add(NULL,NULL);

t4.add(&a,&b);

return 0;
}

重定义和特化
----重定义
·一个类模板和一个新类(或者两个类模板)
·使用的时候需要考虑如何选择的问题

----特化
·以统一的方式使用类模板和特化类
·编译器自动优先选择特化类

工程中的建议

C++第六十课

数组类模板

补充知识
·模板参数可以是数值型参数(非类型参数)

template <typename T,int N>
void func()
{
T a[N];
}

数值型模板参数的限制

——变量不能作为模板参数
——浮点数不能做为模板参数
——类对象不能做为模板参数
——。。。

本质:模板参数是在编译阶段被处理的单元,因此,在编译阶段必须准确无误的唯一确定

示例程序: 最高效的方法求1+2+3+…+N的值

template <int N>
class Sum
{
public:
static const int VALUE = Sum<N-1>::VALUE+N;
};

//完全特化
template <>
class Sum<1>
{
public:
static const int VALUE = 1;
};

int main()
{
Sum<100> s;

cout<<"1+2+3+...+100 = "<<s.VALUE<<endl;  //5050

return 0;
}

类模板本身在编译器编译程序的时候已经被编译过一次了,即Sum类模板里的VALUE的值在编译阶段已经确定,即Sum<100> s中的VALUE值已确定,无需进行加法运算,这是递归定义的,是最高效的

注意
在写数组类的时候,当重载[]操作符的时候要加一个const版本的,因为可能会用该数组类创建一个const对象,则此时只能调用const成员函数
T operator[](int index)const; //返回值类型不能是引用,非const版本才是引用

C++第六十二课

单例模式

需求的提出
在架构设计时,某些类在整个系统生命周期中最多只能有一个对象存在(Single Instance),如某些超市的收钱机

思路
·要控制类的对象数目,必须对外隐藏构造函数和重载的=操作符
——将构造函数的访问属性设置为private
——定义instance并初始化为NULL
——当需要使用对象时,访问instance的值
·空值:创建对象,并用instance标记
·非空值:返回instance标记的对象

示例程序:

template <typename T>
class Singleton
{
private:
static T* c_instance;
public:
static T* GetInstance();
};

T* Singleton<T>::c_instance = NULL;

T* Singleton<T>::GetInstance()
{
if(c_instance == NULL)
{
c_instance = new T();
}

return c_instance;
}

class Object
{
private:
friend class Singleton<Object>;

Object(){}
Object(const Object&);
Object& operator =(const Object&);
public:
//...
};

int main()
{
Object* o = Singleton<Object>::GetInstance();

return 0;
}

思考
为什么不在Object类中直接实现单例模式?
答:因为这样以后使用单例模式的类都要自己再实现一遍相关单例模式的函数,必须定义静态成员变量c_instance,必须定义静态成员函数GetInstance。
所以,将单例模式相关的代码抽取出来,开发单例类模板。当某一个类中需要单例功能时,只需要将单例模板声明为友元类即可

C++第六十四课

C++的异常处理(上)

通过throw抛异常

try和catch
——try语句代码块里执行可能会抛异常的语句
——catch语句处理异常情况
——throw抛出的异常必须被catch处理
·当前函数能够处理异常,程序继续往下执行
·当前函数无法处理异常,则函数停止执行,并返回

·同一个try语句可以跟上多个catch语句
——catch语句可以定义具体处理的异常类型
——不同类型的异常由不同的catch语句负责处理
——try语句中可以抛出任何类型的异常
——catch(…)用于处理所有类型的异常
——任何异常都只能被捕获(catch)一次
——异常处理匹配时,catch里的异常类型不进行任何的类型转换,必须严格匹配
——异常处理匹配时,如果是类对象,父子间的赋值兼容性原则依旧起作用,所以父类异常类型要放到最后面

示例程序:

double divide(double a,double b)
{
double delta = 0.00000001;
double ret = 0;

if(!(-delta<b)&&(b<delta))
{
ret =  a/b;
}
else
throw 0;

return ret;
}
int main()
{
try
{
divide(6,0);
}
catch(char c)
{
cout<<"catch(char c)"<<endl;
}
catch(short s)
{
cout<<"catch(short s)"<<endl;
}
catch(int i)
{
cout<<"catch(int i)"<<endl;  //会执行这个
}
catch(...)
{
cout<<"catch(...)"<<endl;
}

return 0;
}

divide函数中抛出0,0是整型,虽然可以隐式转换为char、short,但是catch异常的时候是严格匹配的,不进行隐式类型转换
特别注意char和const char,如果抛出字符串字面量则该异常类型为const char*,所以捕获异常的时候catch里的类型必须是const char才能匹配上,char是不行的

C++第六十五课

C++的异常处理(下)

catch语句块中可以抛出异常
——需要外层的try…catch…捕获
——在工程中的其中一个目的是重新解释异常,统一异常类型

示例程序:

//第三方库的函数
/*
假设:
0==》参数异常
1==》指针异常
2==》边界异常
3==》未知类型异常
*/
void otherFunc(int i)
{
if(0==i)
throw 0;
else if(5==i)
throw 1;
else if(11==i)
throw 2;
else
throw 3;
}

//自己的库函数
void myFunc(int i)
{
try
{
otherFunc(i);
}
catch(int i)
{
switch(i)
{
case 0:
throw "参数异常";
break;
case 1:
throw "指针异常";
break;
case 2:
throw "边界异常";
break;
case 3:
throw "未知类型异常";
default:break;
}
}
}

int main()
{
try
{
myFunc(5);
}
catch(const char* cs)
{
cout<<"异常类型:"<<cs<<endl;  //指针异常
}

return 0;
}

注意

C++第六十六课

类型识别



示例程序:

class Test
{

};

int main()
{
int a = 0;

const type_info& t = typeid(Test);
const type_info& t2 = typeid(a);

cout<<t.name()<<endl;
cout<<t2.name()<<endl;

return 0;
}

注意
不同编译器的输出结果可能不一样

C++第六十七课

经典问题解析五

补充

·C++也支持变参函数,只不过是从C语言迁移过来的,即它没有C++的面向对象特性
·函数匹配优先级:重载函数 --> 模板函数 -->变参函数

面试题一
如果判断一个变量是不是指针类型?

方法:利用函数匹配优先级

template <typename T>
void isPtr(T* t)
{
cout<<"Para t is a pointer"<<endl;
}

void isPtr(...)
{
cout<<"Para t is  not a pointer"<<endl;
}

class Test
{
public:
Test(){}
~Test(){}
};

int main()
{
int a;
double* d = NULL;

isPtr(a);  //Para t is  not a pointer

isPtr(d);  //Para t is a pointer

Test t1;
Test* t2 = new Test();

isPtr(t1);  //非法指令

isPtr(t2);  //非法指令

return 0;
}

结果分析
对于基本类型,利用函数匹配优先级可以完成这个题目,但是对于自定义类型,比如Test类,运行的时候就会报非法指令。因为变参函数是从C语言迁移过来的,即它没有C++的特性,即它不认识类是什么东西

改进

#define ISPTR(a) (sizeof(isPtr(a))==1)

template <typename T>
char isPtr(T* t)
{
return 't';
}

int isPtr(...)
{
return 0;
}

class Test
{
public:
Test(){}
~Test(){}
};

int main()
{
int a;
double* d = NULL;

cout<<"a is a pointer:"<<ISPTR(a)<<endl;  //0

cout<<"d is a pointer:"<<ISPTR(d)<<endl;  //1

Test t1;
Test* t2 = new Test();

cout<<"t1 is a pointer:"<<ISPTR(t1)<<endl;  //0

cout<<"t2 is a pointer:"<<ISPTR(t2)<<endl;  //1

return 0;
}

分析
改进之前的程序编译通过但是运行时会报非法指令是因为如果函数参数是类对象,则可变参数函数运行时会报错,为了在程序运行前就得出答案,则利用sizeof标识符只在编译期间起作用的特性,改进了上述程序

构造中的异常

·构造函数中抛出异常
——构造过程立即停止
——当前对象无法生成
——析构函数不会被调用
——对象所占用的空间立即收回
——new关键字不返回任何东西

示例程序:

class Test
{
public:
Test()
{
throw 0;
}
~Test(){}
};

int main()
{
Test* t = reinterpret_cast<Test*>(1);

try
{
t = new Test();
}
catch(...)
{
cout<<"t = "<<t<<endl;  //t = 0x1
}

return 0;
}

结果t的值仍然为0x1,说明在用new关键字创建Test对象时失败了,且new没有给t赋任何值,t仍然为最初的0x1

工程中的建议
——不要在构造函数中抛出异常
——当构造函数可能产生异常时,使用二阶构造模式

析构中的异常
在析构函数中抛异常会导致对象所使用的资源无法完全释放,即可能产生内存泄漏等问题

C++第六十八课

拾遗:令人迷惑的写法

typename 和 class
·历史原因
——早期的C++中没有typename,在泛型编程中复用class
如:template

隐患
示例程序:

int a = 0;

class Test
{
public:
struct S
{
int a;
};
};

template <class T>
void func()
{
T::S* a;  //①
}

int main()
{
func<Test>();
return 0;
}

编译报错:error: dependent-name ‘T:: S’ is parsed as a non-type, but instantiation yields a type

原因分析
对于标记①有两种解读:
1.定义一个指针,其类型为T::S并命名为a
2.将值T::S与值a相乘

因为定义模板函数的时候用的是早期的class来声明T,所以编译器不知道T::S是个类型

修正

template <class T>
void func()
{
typename T::S* a;
}

在T::S之前加typename关键字说明后面的T::S是个类型

try和catch
·try…catch用于分隔正常功能代码与异常处理代码
·try…catch可以直接将函数实现分隔为2部分
·函数声明和定义时可以直接指定可能抛出的异常类型
·异常声明成为函数的一部分可以提高代码可读性

示例程序:

新型写法

void func()try
{
//...
}catch()
{

}

在函数声明后面加throw
void GetTag() throw(); // 表示不会抛出任何类型异常
void GetTag() throw(int); // 表示只抛出int类型异常
void GetTag() throw(int,char); // 表示抛出in,char类型异常void GetTag()
throw(…); // 表示抛出任何类型异常

函数异常声明的注意事项
——函数异常声明是一种与编译器之间的契约
——函数声明异常后就只能抛出声明的异常
·抛出其他异常将导致程序运行终止
·可以直接通过异常声明定义无异常函数

C++第六十九课

加粗样式new出来的对象一定在堆上吗
不!

示例程序:
指定在静态存储区创建对象

class Object
{
private:
static const unsigned int count = 4;
static char buf[];
static char map[];

public:

//重载new最好在类中,且返回值类型必须是void*
void* operator new(unsigned int size)
{
void* ret = NULL;

for(int i=0;i<count;i++)
{
if(map[i]==0)
{
ret = buf + sizeof(Object)*i;

map[i] = 1;

cout<<"Successfully to creat an Object..."<<endl;

break;
}
}

return ret;
}

void operator delete(void* p)
{
if(p!=NULL)
{
char* mem = reinterpret_cast<char*>(p);
int index = (mem - buf)/sizeof(Object);
int flag = (mem - buf)%sizeof(Object);

if((flag==0)&&(0<=index)&&(index<=count))
{
map[index] = 0;

cout<<"You had deleted an Object..."<<endl;
}
}
}
};

char Object::buf[sizeof(Object)*Object::count] = {0};
char Object::map[Object::count] = {0};

int main()
{
Object* ob[5] = {0};

for(int i=0;i<5;i++)
{
ob[i] = new Object;
}

for(int i=0;i<5;i++)
{
cout<<"ob["<<i<<"] = "<<ob[i]<<endl;
}

for(int i=0;i<5;i++)
{
delete ob[i];
}

for(int i=0;i<5;i++)
{
ob[i] = new Object;
}

for(int i=0;i<5;i++)
{
cout<<"ob["<<i<<"] = "<<ob[i]<<endl;
}

return 0;
}

两次的地址都一样且地址都是连续的,说明new出来的对象在静态存储区

指定在栈上创建对象
示例程序:

class Test
{
private:
static unsigned int m_count;
static char* m_buf;
static char* m_map;

public:
static bool setMemoryLocation(char* memory,unsigned int size)
{
bool ret = false;

m_count = size / sizeof(Test);

ret = (m_count)&&(m_map = reinterpret_cast<char*>(calloc(m_count,sizeof(char))));

if(ret)
{
m_buf = memory;
}
else
{
free(m_map);

m_count = 0;
m_buf = NULL;
m_map = NULL;
}

return ret;
}

void* operator new(unsigned int size)
{
void*  ret = NULL;

if(m_count>0)
{
for(int i = 0;i<m_count;i++)
{
if(m_map[i]==0)
{
ret = m_buf + sizeof(Test)*i;

m_map[i] = 1;

cout<<"Successfully creat a Test object... It's address is "<<ret<<endl;

break;
}
}
}
else
{
ret = malloc(size);
}

return ret;
}

void operator delete(void* p)
{
if(p!=NULL)
{
if(m_count>0)
{
char* mem = reinterpret_cast<char*>(p);

int index = (mem-m_buf)/sizeof(Test);

int flag = (mem-m_buf)%sizeof(Test);

if((flag==0)&&(0<=index)&&(index<=m_count))
{
m_map[index] = 0;

cout<<"You had deleted a Test object successfully..."<<endl;
}
}
else
{
free(p);
}
}
}

};

unsigned int Test::m_count = 0;
char* Test::m_buf = NULL;
char* Test::m_map = NULL;

int main()
{
char buf[10];

Test::setMemoryLocation(buf,sizeof(buf));

Test* t[100];

for(int i=0;i<100;i++)
{
t[i] = new Test;
}

return 0;
}

拾遗:new和new[] delete和delete[]
1.new和new[]是两个不同的操作符,delete和delete[]同理
2.用new[]申请到的内存空间大小可能比我们期望的要大,因为要用一段额外的内存空间来存储数组大小信息

示例程序:

class Test
{
private:
int m_value;

public:
Test(){}
~Test(){}

void* operator new(unsigned int size)
{
cout<<"new size = "<<size<<endl;

return malloc(size);
}

void operator delete(void* p)
{
cout<<"delete p = "<<p<<endl;

free(p);
}

void* operator new[](unsigned int size)
{
cout<<"new[] size = "<<size<<endl;

return malloc(size);
}

void operator delete[](void* p)
{
cout<<"delete[] p = "<<p<<endl;

free(p);
}
};

int main()
{
Test* t = NULL;

t = new Test();
delete t;

t = new Test[5];
delete[] t;

return 0;
}


new[] 出来的空间多了 4 个字节

外传篇 第一课

异常处理深度解析

问题1

在main函数里抛出异常但未处理会怎样?
----如果异常无法被处理,terminate()结束函数会被自动调用
----默认情况下,terminate()调用库函数abort()终止程序
----abort()函数使得程序执行异常而立即退出
----C++支持替换默认的terminate()函数实现

terminate函数的替换

1.#include
2.自定义一个无返回值无参数的函数
·不能抛出任何异常
·必须以某种方式结束当前程序
3.调用set_terminate()设置自定义的结束函数
·参数类型为void(*)()
·返回值为默认的terminate()函数入口地址

示例程序:

#include <exception>

using namespace std;

class Test
{
public:
Test()
{
cout<<"Test()"<<endl;
}
~Test()
{
cout<<"~Test()"<<endl;
}
};

void myTerminate()
{
cout<<"myTerminate"<<endl;

exit(1);
}

int main()
{
set_terminate(myTerminate);

static Test t;

throw 1;

return 0;
}

问题2

在问题1的基础上,Test类的析构函数抛出异常且main函数不能处理会怎样?
----资源不能释放,从而泄漏
----会导致terminate函数二次调用
----terminate函数调用的时候,其中的exit(1)语句会自动析构所有的全局对象和静态局部对象,即会释放资源。如果对象t的析构函数中抛出异常,main函数还是无法处理,则会再次调用terminate函数,二次调用的时候exit(1)又会再次释放资源,则会造成有些资源的多次释放,不同的编译器会产生不同结果,这是不可预料的

示例程序:

在上述问题1的代码中,Test类的析构函数体的最后加入throw 1;语句,则程序的执行结果为

外传篇 第二课

函数的异常规格说明

问题1

限定了函数抛出的异常类型,但是在函数体里抛出不匹配的异常类型会怎样?
·函数抛出的异常不在规格说明中,全局unexpected()被调用
·默认的unexpected()函数会调用全局的terminate()函数
·可以自定义函数替换默认的unexpected()函数实现

注意:不是所有的C++编译器都支持这个标准行为

unexpected函数的替换

----自定义一个无返回值无参数的函数
·能够再次抛出异常
√ 当异常符合触发函数的异常规格说明时,恢复程序执行
√ 否则,调用全局terminate函数结束程序
----调用set_unexpected()设置自定义的异常函数
·参数类型为void(*)()
·返回值类型为默认的unexpected()函数的入口地址

示例程序(标准行为):

#include <exception>

using namespace std;

void myUnexpected()
{
cout<<"myUnexpected()"<<endl;

//1符合func()的异常规格int,则可以被catch到
throw 1;
}

//'c'不符合int,则自动调用替换后的myUnexpected()
void func()throw(int)
{
throw 'c';
}

int main()
{
set_unexpected(myUnexpected);

try
{
func();
}
catch(int)
{
cout<<"catch(int)"<<endl;
}
catch(char)
{
cout<<"catch(char)"<<endl;
}

return 0;
}

外传篇 第三课

动态内存申请的结果

事实

1.malloc函数申请失败时返回NULL值
2.new关键字申请失败时(根据编辑器的不同)
·可能是返回NULL
·可能是抛出std::bad_alloc异常

new关键字在C++规范中的标准行为

----在堆空间申请足够大的内存
·成功:
√ 在获取的空间中调用构造函数创建对象
√ 返回对象的地址
·失败
√ 抛出std::bad_alloc异常
----new在分配内存时
·如果空间不足,会调用全局的new_handler()函数
·new_handler()函数中抛出std::bad_alloc异常
----可以自定义new_handler()函数
·处理默认的new内存分配失败的情况

如:

#include <new>
#include <exception>
void my_new_handler()
{
//...
}

int main()
{
set_new_handler(my_new_handler);
return 0;
}

问题1
如何跨编译器统一new的行为,提高代码移植性?

示例程序:

#include <iostream>
#include <new>
#include <cstdlib>
#include <exception>

using namespace std;

class Test
{
int m_value;
public:
Test()
{
cout << "Test()" << endl;

m_value = 0;
}

~Test()
{
cout << "~Test()" << endl;
}

//不抛出任何异常,失败时统一返回NULL
void* operator new (unsigned int size) throw()
{
cout << "operator new: " << size << endl;

// return malloc(size);

return NULL;
}

void operator delete (void* p)
{
cout << "operator delete: " << p << endl;

free(p);
}

//不抛出任何异常,失败时统一返回NULL
void* operator new[] (unsigned int size) throw()
{
cout << "operator new[]: " << size << endl;

// return malloc(size);

return NULL;
}

void operator delete[] (void* p)
{
cout << "operator delete[]: " << p << endl;

free(p);
}
};

void my_new_handler()
{
cout << "void my_new_handler()" << endl;
}
void ex_func_1()
{
new_handler func = set_new_handler(my_new_handler);

try
{
cout << "func = " << func << endl;

if( func )
{
func();
}
}
catch(const bad_alloc&)
{
cout << "catch(const bad_alloc&)" << endl;
}
}

void ex_func_2()
{
//如果Test类中重载的new没有限制不抛出任何异常
//则这条语句执行过后会段错误
//因为我们指定new返回NULL,在0地址处调用构造函数是非法的
Test* pt = new Test();

cout << "pt = " << pt << endl;

delete pt;

pt = new Test[5];

cout << "pt = " << pt << endl;

delete[] pt;
}

void ex_func_3()
{
//单次指明new不抛出异常
int* p = new(nothrow) int[10];

// ... ...

delete[] p;

int bb[2] = {0};

struct ST
{
int x;
int y;
};

//new的新用法,指定在bb地址处创建对象
ST* pt = new(bb) ST();

pt->x = 1;
pt->y = 2;

cout << bb[0] << endl;
cout << bb[1] << endl;

//指定在某地址创建对象要手动调用析构函数
pt->~ST();
}

int main(int argc, char *argv[])
{
// ex_func_1();
// ex_func_2();
// ex_func_3();

return 0;
}

附加知识

1.通过函数模板实现求数组长度

template <typename T,int N>
int Array_size(T(&array)[N])
{
return N;
}

解析:当把一个数组当作引用传进去的时候,其长度信息也会传进去。注意,因为[]的优先级比&高,所以要写成(&array),如果写成T& array
的话,那么此时的意思是array的元素类型是T&,长度为N,而实际上我们想表达的是array是个数组引用,其元素类型为T,长度为N

2.new 重载的placement版本

void* operator new(unsigned int size,void* location)
{
return location;
}

这是在指定内存地址location上构造要new的对象,使用方法:

int main()
{
unsigned char space[1024];
Test* t = new(space)Test();

return 0;
}

在space(栈上)构造Test对象,再返回该地址给t

后续添补
1.在编写数据结构代码的时候,写到通用树的部分,写测试代码的时候,在测试函数里定义了一个LinkList<GTreeNode> child对象进行测试,测试的时候传通用树对象的m_root根节点进去,本以为测试函数调用的过程中直到调用结束后都不会修改实参的值,但是此时忽略了LinkList单链表的特性,测试函数中child对象是个临时对象,函数调用结束后会调用LinkList单链表的析构函数,而析构函数又会调用clear函数,把实参中各个结点的堆空间都释放掉了,从而导致在调用测试函数后在后续的代码中都读取不到实参中树的各个结点值。
最后找到了原因,child对象没有定义为引用或者指针,即操作它不会更新实参的内容(比如node->m_child的length等等),在它析构后释放了堆空间,但是实参的链表信息并没有跟着更新(比如还以为node->m_child的长度仍然为length等等),如果在后续对实参进行操作,有很大可能会造成操作非法地址或者操作空指针的错误。
解决方法,将child临时对象定义为LinkList<GTreeNode>,即一个指针,它析构的时候只会回收一个指针大小字节的内存,而不会影响实参的链表;或者将child定一为LinkList<GTreeNode>&,即一个引用,它修改其所代表的单链表的内容的时候,实参也跟着改变。

测试代码如下:(有错误的)

无错误的:

或者

2.当protected或private继承时,基类指针不能直接指向子类指针,要强制类型转换才成功。

  • 点赞 1
  • 收藏
  • 分享
  • 文章举报
苏瓜皮 发布了17 篇原创文章 · 获赞 1 · 访问量 1501 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: