一、从C到C++及类与对象
C++文件命名常为:xxx.cpp、xxx.c++
编译:g++ xxx.cpp
1.1、语法升级
1.1.1、引用
引用 : 相当于给变量取别名 ;
引用的本质是指针常量,指针常量必须要初始化,所以引用必须要初始化。
C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占的空间大小与指针变量相同,只是这个过程编译器内部实现,用户不可见。(const离谁近,修饰谁)
int a = 0;
int &b = a;//自动转换为:int * const b = &a;
b = 1; //内部发现b是引用,自动帮我们转换为:*b = 1;
1、基本类型数据的引用:
#include <stdio.h>
#include <iostream>
using namespace std;
int main(void)
{
int a = 100;
int &b = a;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("a_addr = %p\n", &a);
printf("b_addr = %p\n", &b);
}
运行结果为:
a = 100
b = 100
a_addr = 000000000022FE44
b_addr = 000000000022FE44
通过打印信息发现:a、b的值相等,同时a、b的地址也相同。
注意:引用必须在定义变量的时候时候!!!
一旦一个引用被初始化后,不得再指向其他对象!!!
2、数组的引用:
int arr[10];
int (&ARR)[10] = arr;
或
typedef int ARR[10];
ARR &a = arr;
3、指针的引用
#include <stdio.h>
#include <iostream>
using namespace std;
int main(void)
{
int a = 0;
int *p1 = &a;
*p1 = 1;
int* &p2 = p1;//此时的p2是一个二级指针
cout << "*p1 = " << *p1 << endl;
cout << "*p2 = " << *p2 << endl;
return 0;
}
运行结果为:
*p1 = 1;
*p2 = 1;
4、常变量的引用:
/*引用必须引一块合法的内存空间*/
int &a = 1; 错误
const int &a = 1; 正确,编译器的处理方式为:int tmp = 1; const int &a = tmp;
const虽然能防止a中的值发生变化,但并不是绝对不变的。
#include <stdio.h>
#include <iostream>
using namespace std;
int main(void)
{
const int &a = 1;
int *p = (int *)&a;
cout << "a: " << a << endl;
*p = 2;
cout << "a: " << a << endl;
return 0;
}
运行结果为:
a: 1
a: 2
从上述代码可知,指针对于数据的修改拥有管理员般的权限,只要知道内存单元的地址,通过指针就可完成对内存单元的修改。
const只是修饰某个变量,而不是存储单元地址。
引用的使用:
1、作为形参
#include <stdio.h>
#include <iostream>
using namespace std;
void swap(int &a, int &b){
a ^= b;
b ^= a;
a ^= b;
}
int main(void){
int a = 100;
int b = 10;
swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
运行结果为:
a = 10
b = 100
如果形参是单纯的a、b,则函数调用后,实参变量还是没有发生变化,如果形参变量以指针的形式,则在形式上比较混乱,C++中“引用”的引入,提供了一种更为简洁的方案,不产生新变量,减少实参和形参的拷贝开销。
2、返回引用(返回值类型是引用类型),可以作为左值使用
#include <stdio.h>
#include <iostream>
using namespace std;
int & func(void)
{
static int a = 0;
return a;
}
int main(void)
{
cout << func() << endl;
func() = 10;
cout << func() << endl;
return 0;
}
运行结果为:
0
10
注意:当返回类型为引用时,该引用所对应的变量是否被销毁。
1.1.2、默认参数
例如:
void debug(const char *p = "hello world");
调用该函数时,若为
debug(); //则用默认的参数
若为
debug("hi"); //则用新的实参
注意:若有多个形参,带默认参数的应从右向左排放!!!
如
void test(int a, int b, int c = 1);
1.1.3、函数重载
允许函数名相同,编译器可根据形参的不同(个数、类型、顺序)编译链接系统确认调用哪个函数。函数返回值不同,而函数头部其它部分相同,则认为重复定义。
1.1.4、堆内存
堆内存:malloc()、free() ——> new()、delete
/*c*/
char *p = (char *)malloc(10);
strcpy(p, "hello");
printf("p = %s\n", p);
free(p);
/*cpp 单颗粒内存*/
int *p = new int;
*p = 10;
printf("*p = %d\n", *p);
delete p;
/*cpp 堆内存*/
char *p = new char[10];
strcpy(p, "hello");
printf("p = %s\n", p);
delete [] p;
1.2、类与对象
在C语言中数据的封装采用结构体;算法的封装采用函数。
如何用C将数据和算法封装在结构体中?函数指针
#include <stdio.h>
#include <iostream>
using namespace std;
struct arr;
struct arr{
int a[32];
int end;
void (*add)(struct arr * list, int data, int end);//为避免在其它文件中被调用,可在指针前加上static
void (*show)(struct arr * list, int end);
};
void arr_add(struct arr * list, int data, int end)
{
list->a[end] = data;
}
void arr_show(struct arr * list, int end)
{
int i = 0;
for(i = 0; i <= end; i++){
printf("%d, ", list->a[i]);
}
printf("\n");
}
int main(void)
{
struct arr value;
/*结构体变量初始化*/
value.add = arr_add;
value.show = arr_show;
value.add(&value, 1, 0);
value.show(&value, 0);
return 0;
}
运行结果为:
1,
这种方法有三个缺点:
1、由于结构体内不能初始化,所以在定义结构体变量时,一定要将函数指针初始化,这体现不出面向对象的编程。(C++构造函数解决)
2、结构体变量的数据成员以其它形式也可以修改,没有安全性。(只要知道变量的地址,其它函数都可以修改)。(C++中public/private/protected解决)
3、用函数指针的时候还要传实参,挺累赘(已经知道对象了,应该能自己找到实参)。
基于这三个缺点出现了C++中的类与对象的概念。
1.2.1、类的声明
class 类名
{
private:
私有的数据和成员函数;
public:
公用的数据和成员函数;
protected:
保护的数据和成员函数;
};//注意这个分号不要忘了!!!
private:只能由该类中的函数、其友元函数访问,不能被其它访问,该类的对象也不能访问。
protected:可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问。
public:可以被该类中的函数、子类的函数、以及其友元函数访问,也可以被该类的对象访问。
注意: 若没有写private、public、protected,则类中的数据成员和成员函数默认为private,struct结构体中的成员默认为public!!!
C++代码示例:
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
void show(void){
printf("xxxxxx\n");
}
void setdata(int data){
a = data;
}
int getdata(void);
private:
int a;
};
int A::getdata(void)
{
return a;
}
int main(void)
{
A x;
x.setdata(100);
printf("%d\n", x.getdata());
x.show();
}
类中应该包含:构造函数、析构函数、成员函数、数据成员;
类将算法和数据有机的结合起来。
1.2.2、类的构造函数
A(){};
A x; //每当生成一个x变量的时候,系统都会自动调用一次构造函数,如果没有构造函数系统会调用默认的(默认的构造函数什么都不做),如果定义了构造函数,系统就会调用定义好的构造函数,默认的构造函数将不再被调用。
/*构造函数*/
//形式1
A(int data)
{
a = data;
}
//形式2——初始化表
A(int data):a(data);
/*完成参数的传递*/
A x(100);
或
A x = 100;//隐式调用构造函数
一个类中允许多个构造函数,因为C++支持函数重载。
构造函数的作用:类的初始化,创建类时自动调用。
注意: 当类中定义了带参数的构造函数,如果在定义对象过程中使用无参的形式,如A x; 这种形式则编译会出错,系统不会选默认的构造函数,它希望自己申明一个无参的构造函数或者带默认参数的构造函数。
/*无参对象的定义*/
A x;
A x();//这种写法是错误的,不用加()
/*带参数对象的定义*/
A x(100);
1.2.3、类的析构函数
类的析构函数: 对象销毁时系统自动调用
~A(){}; //()中不带参数!!!
this指针:类对象自己本身的一个指针,指向自己。
当两个变量重名时,如何区分哪个是类中的数据?加this指针
void arr::addtail(int data)
{
this->data[tail++] = data;
}
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
A(){
printf("A() function\n");
}
A(int data){
this->data = data;
printf("A(int data) function, data = %d\n", this->data);
}
~A(){
printf("~A() function\n");
}
private:
int data;
};
int main(int argc, const char *argv[])
{
A x;
A m(100); //A *p = new A(100); 在堆中创建
A y=10; //隐式地调用构造函数实现一次构造
A z=y; //拷贝构造函数(默认的)
return 0;
}
运行结果为:
A() function
A(int data) function,data = 100
A(int data) function,data = 10
~A() function
~A() function
~A() function
~A() function
1.2.4、深浅拷贝问题
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
A(){
printf("A()\n");
p = new char[10];
}
~A(){
printf("~A()\n");
delete [] p;
}
private:
char *p;
};
int main(void)
{
A x;
A y = x;//运行的时候会出现段错误,因为一个内存空间内,同一个地址被析构函数释放了两次
return 0;
}
以上的代码说明了C++中深浅拷贝问题。
浅拷贝只是对类中的数据进行了拷贝,而对数据(指针)指向的内存内容没拷贝,并且拷贝过程中只调用拷贝构造函数而不调用构造函数。
深拷贝是将类中的数据以及数据(指针)指向的内容也进行拷贝。
浅拷贝
深拷贝
解决办法:深拷贝
/*拷贝构造函数*/
A(const A &x)//注意:必须加 &(引用),为了让编译系统识别
{
printf("A(const A &x)\n");
p = new char[10];
strcpy(p, x.p);
}
注意: xxx构造函数什么时候被调用,对象构建的那一刻才会被调用,y=x是不会调用xxx构造函数的。
xxx构造函数调用时间:创建对象的时候!!!
注:xxx表示用于初始化的构造函数或拷贝构造函数。
所以拷贝构造函数只是解决了int y = x的拷贝问题,而没有解决y = x的拷贝问题,这个问题由下文中赋值运算符的重载来解决。
拷贝构造函数和构造函数还是有一定的区别:
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
A(int data):a(data){}
private:
int a;
};
int main(void)
{
A a(10);//编译通过
A b = a;//编译通过,调用默认的拷贝构造函数
A c; //编译不通过,因为构造函数已经自定义了,默认的构造函数不起作用,所以编译是没有匹配成功
return 0;
}
说明:默认拷贝构造函数和默认构造函数是两种体系。
1.2.5、类的成员函数
类的成员函数有两种定义的方式。
/*方式一:类内定义*/
class A
{
void func(void){
printf("hello world\n");
}
};
/*方式二:类外定义*/
class B
{
void func(void);
};
void B::func(void){
printf("hello world\n");
}
1.2.6、常成员
常成员 :C++推荐const而不用#define,mutable
!!!const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的(static除外)。
1、常数据成员(只能构造函数初始化表赋值)
class A{
public:
A(int data):x(data){}
const int x;
};
/*错误的构造函数*/
/*
class A{
public:
A(int data){
x = data;
}
const int x;
};
*/
2、常成员函数
void fun() const;//类中声明的数据不允许在函数内的变化
3、常对象
const A a;//对象中的数据不允许变化
1.2.7、静态成员
静态成员(属于类不属于对象)
普通的成员函数必须依附于对象才能调用。
如果函数脱离了对象也能调用,可以在函数前加static,变成静态成员函数。
静态数据成员初始化
必须在类外初始化:
static int A::x = 10;
使用有两种方式:
/*方式一:类名::数据成员*/
A::data = 100;
/*方式二:对象名.数据成员*/
A a;
a.data = 100;
例如:
#include <stdio.h>
class A{
public:
static int data;
};
int A::data = 10;
int main(void)
{
A a;
a.data = 100;
printf("a.data = %d\n", a.data);
A::data = 1000;
printf("A::data = %d\n", a.data);
return 0;
}
运行结果为:
a.data = 100
A::data = 1000
静态成员函数(只能访问静态成员)
/*类内定义*/
static void func(void);
或
/*类外定义*/
static void 类名::func(void);
调用方法:
/*形式一:类名.静态成员函数*/
A::func();
/*形式二:对象名.静态成员函数*/
A a;
a.func();
静态成员
1.2.8、友元
友元(破坏封装)
如果一个类把某个函数当成友元函数,那么函数对这个类的成员都可以访问。
友元是单向的,不可传递的一种关系。
注:友元函数包括:a.设为友元的全局函数;b.设为友元类中的成员函数。
1.友元类 friend class B;
2.友元函数 friend void func();(func不属于任何类)
3.友元成员函数 friend void B::func();(func属于类B)
#include <stdio.h>
class A;//在使用时,类需要声明一下,为fun2做准备
void fun2(A &x);//fun2声明一下,为类的定义做准备
class A{
public:
static int data;
void fun1(void){
printf("fun1\n");
}
friend void fun2(A &x);
};
int A::data = 10;
/*如果将友元函数的定义放在类定义的前面,那么由于类中具体的
成员如何,编译系统不知道,所以会报错。
因此友元函数的使用需要遵循一下步骤:
类的声明->友元函数的声明->类的定义->友元函数的定义
*/
void fun2(A &x)//必须通过形参传入A的对象!!!
{
x.fun1();
}
int main(void)
{
A a;
fun2(a);
return 0;
}
友元虽然可以使用类中的成员,但是不同于类中的成员函数在函数体中直接使用,友元需要通过传递对象的实参的方式使用类中的成员。
二、运算符重载及组合与继承
2.1、运算符重载
2.1.1、概念
C++准许以运算符命名的函数!!!
string a = "hello";
a += "world"; // +(a, "world"); 字符串变量a后加个world字符串,类似strcat函数
cout << "hello";// <<(cout, "hello");
2.1.2、不可重载的运算符
大部分运算符都是可重载的,只有部分运算符不可重载,其中包括:‘.’、‘.*’、‘::’、‘?:’。
+的运算符重载(成员函数式)
#include <stdio.h>
#include <iostream>
using namespace std;
class Time{
public:
Time(int a = 10, int b = 10, int c = 10):hour(a),min(b),sec(c){}
~Time(){}
Time operator+(Time t){
Time tmp;
tmp.hour = hour + t.hour;
tmp.min = min + t.min;
tmp.sec = sec + t.sec;
return tmp;
}
void show(void){
printf("%d:%d:%d\n", hour, min, sec);
}
private:
int hour;
int min;
int sec;
};
int main(void)
{
Time a,b;
Time c = a + b;
a.show();
b.show();
c.show();
return 0;
}
运行结果为:
10:10:10
10:10:10
20:20:20
i++的运算符重载(成员函数式)
Time operator++(int)
{
Time tmp = *this;
sec++;
return tmp;
}
++i的运算符重载(成员函数式)
Time operator++()
{
sec++;
return *this;
}
规定:为了让编译器知道一个是前加一个是后加所以运算符重载的时候形参中加入int作为标识。
注意:tmp的变量的使用是为了实现前加和后加。
[]的运算符重载(成员函数式)
class Time{
...
int & operator[](int i)
{
switch(i){
case 0: return hour; break;
case 1: return min; break;
case 2: return sec; break;
}
}
...
};
int main(void)
{
Time a;
printf("hour:%d\n", a[0]);
a[0] = 1;//注意:如果函数返回值不是引用则不允许这样操作,因为左操作数是可被修改的。
}
()的运算符重载(成员函数式)
/*仿函数*/
double operator()(double rmb)
{
return rmb*rate;
}
使用:
类为convert
convert RMBtoUS(6.4); //注意:这两个括号的差异,一个是初始化,一个是重载
count << RMBtoUs(10) << endl;
之前深浅拷贝问题还存在一个bug:通过构造函数解决深浅问题,而此时的场景是定义一个对象的时候,即A a = b; 隐式的调用拷贝构造函数,如果是a = b如何解决深浅拷贝问题——赋值运算符的重载。
=的运算符重载(成员函数式)
A & operator=(A &x)
{
p = new char[10];
strcpy(p, x.p);
return *this;
}
使用:
A x, y;
x = y;
2.1.3、命名空间
项目中存在重名的符号,通过命名空间进行区分。
using namespace std;//开放命名空间
cout << "hello" << endl;//cout为对象,endl表示换行
std::cout << "hello" << std::endl;//没有开放命名空间时
// 定义命名空间
namespace A{
void func(){
cout << "A-func" << endl;
}
}
int main()
{
// 方式一
using namespace A;
func();
// 方式二
A::func();
}
- 全局变量 a 表达为
::a
,用于当有同名的局部变量时来区别两者。
- 如果输入a,如果存在局部变量,那么使用局部变量,如果没有局部变量,那么使用全局变量,如果命名空间中有a,且using了该命名空间,那么就会出现定义相同名称的全局变量;
- 命名空间允许嵌套,比如A嵌套B,则使用B命名空间的符号时:
using namespace A::B;
- 不连续的命名空间,允许多处对同名的命名空间进行创建,作为补充,但是不能创建同名变量或函数。
详细参考:C++ 命名空间 🚀
重点重点重点
①运算符重载函数为类成员函数,则c1+c2,编译系统理解为c1.operator+(c2);
②如果为友元函数,编译系统则理解为operator+(c1, c2);(可交换)
所以如果为成员函数,第一个对象必须是与operator+相同的类的对象。
友元函数式(左操作数不是本身,可交换型)
ostream & operator<<(ostream &out, const Time &x)
{
out << "hour:" << x.hour << " min:" << x.min << " sec" << x.sec << endl;
}
2.1.4、标准输入输出流
/*头文件*/
#include <iostream>
using namespace std;
cout << "output";
cout << 10;
cout << hex << 10;//以16进制输出,使用控制符控制输出格式
cin >> buf; //输入
cout.unsetf(ios::dec);//中止十进制
输出格式的选择有两种:
a.使用控制符控制输出格式;
b.用流对象的成员函数控制输出格式。
2.2、组合与继承
组合就是一个类作为另一个类的数据成员。
继承又称为派生,在继承原有类的情况下,升级成新的类。
例如:
class A:public B{
...
};
其中A为派生类,B为基类,public为继承方式
2.2.1、基类成员在派生类中的访问属性
基类中的成员 | 在公用派生类中 | 在私有派生类中 | 在保护派生类中 |
---|---|---|---|
私有成员 | 不可访问 | 不可访问 | 不可访问 |
公用成员 | 公用 | 私有 | 保护 |
保护成员 | 保护 | 私有 | 保护 |
1.大部分使用公用继承;
2.基类的私有成员无论如何都无法访问,所以基类最好可以提供函数接口来访问。
三、多态、异常以及转换函数
3.1、多态
基类和派生类存在同一函数名、参数个数和类型相同而功能不同,因为它们不在同一个类中,编译系统按照同名覆盖的原则决定调用的对象。
class A{
void func(void){}
};
class B:public A{
void func(void){}
};
int main(void)
{
B b;
b.func();//调用类B中的func
b.A::func();//调用类A中的func
}
C++中支持基类的指针指向派生类的对象,不需要通过强制类型转换即可通过编译。(上行转换,安全)
基类指针变量->成员,如果这个成员不存在与基类中,则编译出错。
基类指针能访问的也只是基类自己的成员,编译系统根据情况指向派生类的成员,情况如下:
基类指针->虚函数 ---> 调用 ---> 派生类的同名函数
基类指针->函数 ---> 调用 ---> 基类自己的函数
基类指针->数据(地址) = 派生类对象基地址 + 基类数据成员在基类中的偏移
派生类指针指向基类的对象,需要通过强制类型转换通过编译。(下行转换,不安全)
派生类指针->虚函数 ---> 调用 ---> 基类的同名函数
派生类指针->函数 ---> 调用 ---> 派生类自己的函数
派生类指针->数据(地址) = 基类对象基地址 + 派生类数据成员在派生类中的偏移
上述两大情况与下文中的static_cast标准转换函数情况一致。
为什么说上行是安全的而下行是不安全的?
因为当基类指针指向派生类的对象时,指针的类型还是基类,指针无法指向一个基类之外的成员(那么编译会报错),即不存在访问不存在的空间的可能性;而派生类指针指向基类的对象,那么很有可能会访问基类对象之外的空间,这是非常危险的操作,C++中为了降低这种转换的风险,加入了一些库,通过类似于dramatic_cast标准转换函数完成下行转换。
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
void show(void){
printf("class_A\n");
}
private:
protected:
};
class B : public A{
public:
void show(void){
printf("class_B\n");
}
};
int main(void)
{
A *a_ptr;
B b;
a_ptr = &b;
b.show();
a_ptr->show();
return 0;
}
运行结果为:
class_B
class_A
基类的指针如何使用派生类的成员(虚函数):
1.基类中的函数必须声明为虚函数(前面加一个virtual);
2.派生类中的函数必须与基类中的函数头部相同。
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
virtual void show(void){
printf("class_A\n");
}
};
class B : public A{
public:
void show(void){
printf("class_B\n");
}
};
int main(void)
{
A *a_ptr;
B b;
a_ptr = &b;
b.show();
a_ptr->show();
a_ptr->A::show();
return 0;
}
运行结果为:
class_B
class_B
class_A
/*虚函数*/
virtual double getc(void){}
虚函数的作用:允许在派生类中重新定义与基类同名(函数头部相同)的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
如果没有虚函数,那么当基类指针指向派生类对象时,基类指针只能访问基类的数据成员。
/*纯虚函数*/
virtual double getc(void) = 0;
在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用来定义对象。
在用法上,可以通过基类的指针指向派生类的对象,从而通过基类调用派生类的成员。
目的:
a.统一接口
b.强制实现(不允许带纯虚函数的类声明对象)
派生类声明和销毁对象时,对派生类和基类的构造函数和析构函数都会调用一遍。
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
A(){
printf("class_A_start\n");
}
~A(){
printf("class_A_end\n");
}
};
class B : public A{
public:
B(){
printf("class_B_start\n");
}
~B(){
printf("class_B_end\n");
}
};
int main(void)
{
B b;
return 0;
}
运行结果为:
class_A_start
class_B_start
class_B_end
class_A_end
多态性分为两种:静态多态性和动态多态性。
静态多态性又称编译时的多态性,是通过函数的重载实现的;
动态多态性又称运行时的多态性,是通过虚函数实现的。
派生类定义对象,基类初始化:
class A{
public:
A(int a):data(a){}
private:
int data;
};
class B:public A{
public:
B(int a, int b):A(a), value(b){}
private:
int value;
};
派生类对象销毁时,首先调用派生类对象的析构函数,然后由派生类对象的析构函数再调用基类的析构函数。如果基类指针指向new出的派生类对象,那么在delete p的时候,只调用基类的析构函数。
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
~A(){
cout << "A~" << endl;
}
};
class B:public A{
public:
~B(){
cout << "B~" << endl;
}
};
int main(void)
{
A *p = new B;
delete p;//只析构了基类的资源而没有析构派生类的资源
return 0;
}
运行结果:
A~
依据上述基类指针指向派生类对象的情况推测可知,基类指针当调用析构函数时,实际上是调用基类自身的析构函数。
为了解决这个问题,引入虚析构函数,防止内存回收不完全而造成内存泄露。
class A{
public:
virtual ~A(){
cout << "A~" << endl;
}
};
这样的话,基类的指针就指向派生类的析构函数(与上面基类指针指向派生类对象的情况相同),就能完成对派生类和基类的析构。
3.2、异常
goto不允许段外跳转,比如两个函数间不允许跳转。
在发生异常时,往往需要进行一些备份工作,异常的发生有多种可能,而处理方式大同小异,为了实现段外跳转以及减少异常处理代码的重复书写,C++中引入了异常处理机制。
其基本思想是:函数 A 在执行过程中发现异常时可以不加处理,而只是“拋出一个异常”给 A 的调用者且A后面的语句不再执行,假定调用者为函数 B。在这种情况下,函数 B 可以选择捕获 A 拋出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被拋给 B 的调用者,以此类推。
如果一层层的函数都不处理异常,异常最终会被拋给最外层的 main 函数。main 函数应该处理异常。如果main函数也不处理异常,那么程序就会立即异常地中止,try…catch后面的语句不会去执行。
3.2.1、异常处理基本语法
C++通过throw语句和try…catch语句实现对异常的处理。throw语句的语法如下:
throw 表达式;
//该语句抛出一个异常。异常是一个表达式,其值的类型可以是基本类型,如int、string等,也可以是类
1、try…catch语句的语法
try{
语句组
}
catch(异常类型){
异常处理代码
}
...
catch(异常类型){
异常处理代码
}
catch可以有多个,但至少要有一个。
try…catch语句的执行过程是:
①执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
②如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”,这个匹配过程是自动完成的),执行完后再跳转到最后一个 catch 块后面继续执行。
如果没有匹配成功,即拋出的异常就没有 catch 块能捕获,这个异常也就得不到处理,异常就会往外抛(就是所谓的异常再抛出)当前层的try…catch不会被执行,且当前层的try…catch 后面的内容也不会被执行。
注意: 在try花括号内定义的变量,其生存周期和作用域仅限于花括号内,所以谨慎在try花括号内定义变量。(C语言也一样)
#include <stdio.h>
#include <iostream>
using namespace std;
int err(void)
{
throw 1;
}
int main(void)
{
try{
err();
}
catch(int i){
cout << "err_int" << endl;
}
catch(float i){
cout << "err_double" << endl;
}
printf("end\n");
return 0;
}
运行结果为:
err_int
end
2、能够捕获任何异常的 catch 语句
catch(...){
异常处理代码
}
示例代码:
#include <stdio.h>
#include <iostream>
using namespace std;
int err(void)
{
throw "hi";
}
int main(void)
{
try{
err();
}
catch(int i){
cout << "err_int" << endl;
}
catch(float i){
cout << "err_double" << endl;
}
catch(...){
cout << "catch(...)" << endl;
}
printf("end\n");
return 0;
}
运行结果为:
catch(...)
end
注意: catch的匹配是遵循先后的顺序的,当前面的catch匹配成功后,不会去执行后面的catch,而是执行最后一个catch后面的语句。
所以catch(…)要写在所有catch的后面。
3、异常的再抛出
#include <stdio.h>
#include <iostream>
using namespace std;
int err_1(void)
{
try{
throw "err";
}
catch(char i){
cout << "inter_err1_char" << endl;
}
printf("inter_err1_end\n");
return 0;
}
int err_2(void)
{
try{
throw 1;
}
catch(int i){
cout << "inter_err2_int" << endl;
}
printf("inter_err2_end\n");
return 0;
}
int main(void)
{
try{
err_2();
err_1();
}
catch(int i){
cout << "outer_int" << endl;
}
catch(float i){
cout << "outer_double" << endl;
}
catch(...){
cout << "outer_catch(...)" << endl;
}
printf("outer_end\n");
return 0;
}
运行结果为:
inter_err2_int
inter_err2_end
outer_catch(...)
outer_end
throw抛出异常到catch捕获到异常,就要经历一次对象拷贝(要调用拷贝构造函数的过程,当然可以在catch(xxx &i)中使用引用的方式,减少对象拷贝提高效率。
#include <stdio.h>
#include <iostream>
using namespace std;
int err_1(void)
{
try{
throw string("err1_error");//sting("err1_error")是一个无名对象
}
catch(string i){
cout << "inter_err1_sring" << endl;
throw;//继续抛出捕获的异常
}
printf("inter_err1_end\n");
return 0;
}
int main(void)
{
try{
err_1();
}
catch(string i){
cout << "i:" << i << endl;
}
catch(...){
cout << "outer_catch(...)" << endl;
}
printf("outer_end\n");
return 0;
}
运行结果为:
inter_err1_string
i:err1_error
outer_end
上述代码中,throw; 没有指明抛出什么样的异常,因此抛出的就是catch块捕获到的异常,即string(“err1_error”)。这个异常会被main函数中的catch块捕获。
3.2.2、函数的异常声明列表
为了增强程序的可读性和可维护性,使程序员在使用一个函数时就能看出这个函数可能会抛出哪些异常,C++允许在函数声明和定义时,加上它所能抛出的异常的列表,具体写法如下:
void func(void) throw (int, double, A, B, C);
与
void func(void) throw (int, double, A, B, C){}
上面的写法表明func可能抛出int型、double型以及A、B、C三种类型的异常。异常声明列表可以在函数声明时写,也可以在函数定义时写,如果两处都写,则两处应一致。
如果异常声明列表如下编写:
void func(void) throw ();
则说明func函数不会抛出任何异常。
一个函数如果不交代能抛出哪些类型的异常,就可以抛出任何类型的异常。
函数如果抛出了其异常声明列表中没有出现的异常,在编译时不会引发错误,但在运行时,Dev C++编译出来的程序会出错;用Visual Studio 2010 编译出来的程序则不会出错,异常声明列表不起实际作用。
3.2.3、C++标准异常类
C++标准库中有一些类代表异常,这些类都是从exception类派生而来的,exception类所在的头文件为:
#include <exception>
exception被声明为:
class expection{
public:
exception() throw(); //构造函数
exception(const exception &) throw(); //拷贝构造函数
exception & operator= (const exception &) throw(); //运算符重载
virtual ~exception() throw(); //析构函数
virtual const char * what() const throw(); //虚函数
};
what函数返回一个能识别异常的字符串。
#include <stdio.h>
#include <iostream>
#include <exception>
using namespace std;
/*自定义的异常类*/
class myexception: public exception{
public:
const char * what() const throw(){
return "hi";
}
};
int main(void)
{
try{
throw myexception();
}
catch(myexception &e){//注意myexception异常抛出的是一个myexception的引用
cout << e.what() << endl;
}
return 0;
}
3.3、转换函数
所谓转换函数就是完成不同数据类型之间的转换。在C语言中,当一个float类型的变量赋值给int类型的变量时,就完成了一次类型的转换,这个转换在C中已经固定。而在C++中,引入了转换函数,通过自定义转换函数,即可完成不同类型变量间的转换。
C++中继承了C的语法,并在C的基础上进行了扩展。
隐式转换(implicit conversion)
宽化转换
char a = 10;
int b = a;//将a存储单元内的数据,复制到b存储单元
char占用一个字节,int占用四个字节,由char型转成int型是宽化转换(bit位数增多),编译器没有警告,允许直接转换。
窄化转换
int b = 10;
char a = b;
从四个字节的数据存储到一个字节的存储单元中,编译器就会有警告,提醒程序员可能丢失数据。不过需要注意,有些隐式转换,编译器可能并不会给出waring,比如int到short,但数据溢出依然会发生。
!!!从底层的角度来考虑,数据在内存中没有“类型”这么一说,比如某个存储单元可能是字符型,也可能是整型的一部分,也可能是指针变量的一部分。我们定义的是变量的类型,其实就是定义了数据应该“被看成什么”的方式。
1、C风格显式转换(C style explicit conversion)
float a = 1.1;
int a = (int)a;//这种方式也称为强制类型转换
如下代码,如果采用C风格的强制转换,很容易造成内存访问越界。
Notepad++使用g++的编译器运行正常,但是打印错误数字;
visual studio直接异常退出。
这段代码有多个原因导致异常:
1、x,y的地址根据这两个变量在CAddition中的位置,在CDummy对象的基地址的基础上进行偏移得出的。这个地址是非法的。
2、即时这个地址是合法的,而x、y变量没有进行初始化,*x+y的结果也是错误的。
#include <iostream>
using namespace std;
class CDummy{
public:
float i,j;
CDummy():i(100),j(10){}
};
class CAddition: public CDummy{
int *x,y;
public:
CAddition (int a, int b) { x=&a; y=b; }
int result() {
return *x+y;
}
};
int main () {
CDummy d;
CAddition * padd;
padd = (CAddition*) &d;
cout << padd->result() << endl;
return 0;
}
根据前面的两种类型的转换可以看出,不安全来源于两个方面:
①类型的窄化转化,会导致数据位数的丢失;
②在类继承链中,将父类对象的地址(指针)强制转化为子类的地址指针,这就是所谓的下行转换。
“下”表示沿着继承链向下走(向子类的方向走)
上行转换的上表示沿着继承链向上走(向父类的方向走)
结论:
上行转换一般是安全的,下行转换很可能是不安全的。
值得一说的是,不安全的转换不一定会导致程序出错,比如一些窄化转换在很多场合都会被频繁地使用,前提是程序员足够小心以防止数据溢出;下行转换关键看其“本质”是什么,比如一个父类指针指向子类,再将这个父类指针转成子类指针,这种下行转换就不会有问题。
针对类指针的问题,C++特别设计了更加细致的转换方法,分别有:
reinterpret_cast <new_type> (expression)
const_cast <new_type> (expression)
static_cast <new_type> (expression)
dynamic_cast <new_type> (expression)
可以提升转换的安全性。
3.3.1、标准转换函数
1.reinterpret_cast
将一个类型的指针准换为另一个类型的指针;
很少用(危险)
用法示例:
#include <stdio.h>
#include <iostream>
using namespace std;
int main(void)
{
int a = 1;
char *p = reinterpret_cast<char *>(&a);
printf("a = %d\n", *p);
return 0;
}
运行结果为:
a = 1
2.const_cast
const指针与普通指针间的相互转换
很少用(危险)
#include <stdio.h>
#include <iostream>
using namespace std;
int main(void)
{
const int a = 1;
int *p = const_cast<int *>(&a);//相当于 int *p = (int *)&a;
(*p)++;
printf("a = %d\n", *p);
return 0;
}
运行结果为:
a = 2
3.static_cast静态转换
主要用于基本类型间的相互转换,和具有继承关系间的类型准换
#include <typeinfo.h>
#include <iostream>
using namespace std;
class Tfather{
public:
virtual void show(void){
cout << "Tfather" << endl;
}
int data_father;
};
class Tson: public Tfather{
public:
void show(void){
cout << "Tson" << endl;
}
void show1(void){
cout << "Tson1" << endl;
}
int data_son;
};
int main(void)
{
Tfather father;
Tson son;
father.data_father = 0;
son.data_son = 0;
Tfather *pf;
Tson *ps;
/*上行转换*/
cout << "***************The upward transition***************" << endl;
ps = &son;
pf = static_cast<Tfather *>(ps);
printf("pf(addr) : %p\n", pf);
printf("ps(addr) : %p\n", ps);
//函数成员
pf->show(); //Tson(表现为多态性)如果没有虚函数,打印 Tfather
//pf->show1(); 编译出错:没有这个成员
//数据成员
pf->data_father = 2;
printf("data_father: addr = %p, value = %d\n", &(pf->data_father), pf->data_father);
//pf->data_son = 2;编译出错:没有这个成员
/*下行转换*/
cout << endl << "**************The downward transition**************" << endl;
pf = &father;
ps = static_cast<Tson *>(pf);
printf("pf(addr) : %p\n", pf);
printf("ps(addr) : %p\n", ps);
//函数成员
ps->show(); //Tfather 如果没有虚函数,打印 Tson
ps->Tson::show();
ps->show1();//Tson1 Tson1
//数据成员
ps->data_father = 3;
printf("data_father: addr = %p, value = %d\n", &(ps->data_father), ps->data_father);
ps->data_son = 3;
printf("data_son: addr = %p, value = %d\n", &(ps->data_son), ps->data_son);
return 0;
}
运行结果为:
***************The upward transition***************
pf(addr) : 000000000022fe20
ps(addr) : 000000000022fe20
Tson
data_father: addr = 000000000022fe28, value = 2
**************The downward transition**************
pf(addr) : 000000000022fe30
ps(addr) : 000000000022fe30
Tfather
Tson
Tson1
data_father: addr = 000000000022fe38, value = 3
data_son: addr = 000000000022fe3c, value = 3
从运行的结果可知,
1、静态转换并不会改变地址,还是将右值(指针)赋值给左值(指针变量)。
2、静态转换中上行转换符合多态性。
3、下行转换中,无论是派生类数据成员还是成员函数,均能正常访问(越界访问),不同运行平台可能有差异。下行转换符合多态性。
错误示例:
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
};
class B{
public:
void show(void){
cout << "A" << endl;
}
};
int main(void)
{
A a;
B *b = static_cast<B *>(&a);
return 0;
}
编译器就能看到这种非继承关系的类进行指针转换,并判断该操作为不安全的,报出编译错误:
error:invalid static_cast from type 'A*' to type 'B*'
这是一种非继承关系的类型转换,在编译的时候直接报错,如果是强制类型转换的话,编译是可以通过的。所以静态转换对于非继承关系的类型转换的危险操作,直接将问题消灭在了编译过程中,而不是运行时。
4.dynamic_cast
动态转换的使用条件:
一、仅能在继承类对象间转换;
二、下行转换时要求基类是多态的(基类中包含至少一个虚函数);
三、new_type为指针或引用。
dynamic_cast具有类型检查功能,比static_cast更安全。
尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降。
#include <typeinfo.h>
#include <iostream>
using namespace std;
class Tfather{
public:
virtual void show(void){
cout << "Tfather" << endl;
}
};
class Tson: public Tfather{
public:
void show(void){
cout << "Tson" << endl;
}
void show1(void){
cout << "Tson1" << endl;
}
int data;
};
int main(void)
{
Tfather father;
Tson son;
son.data = 1;
Tfather *pf;
Tson *ps;
/*下行转换*/
pf = &son;
ps = dynamic_cast<Tson *>(pf);
if(ps == NULL){
cout << "point is NULL" << endl;
}
else{
cout << "ps: " << ps << endl;
}
/*下行转换*/
pf = &father;
ps = dynamic_cast<Tson *>(pf); //下行转换,如果不存在虚函数,编译报错,存在虚函数,返回NULL给ps
if(ps == NULL){
cout << "point is NULL" << endl;
}
/*上行转换*/
ps = &son;
pf = dynamic_cast<Tfather *>(ps);
if(ps == NULL){
cout << "point is NULL" << endl;
}
pf->show();//Tson,如果没有虚函数,Tfather
return 0;
}
运行结果为:
ps: 0x22fe20
point is NULL
Tson
从运行的结果可知,
1、使用dynamic_cast上行转换时,基类和派生类不需要虚函数,也能通过编译,只是不能实现多态性。
2、使用dynamic_cast下行转换时,要求基类是多态的(基类中包含至少一个虚函数),不然编译会出错;同时父类经过dynamic_cast转成子类的指针是空指针。
3、对象是子类,中间通过父类指针变量进行传递,最终通过dynamic_cast将其转换为子类的指针,这种转换是安全的,对象的本质是子类,转换的结果使子类指针指向子类。
特殊情况:
#include <iostream>
using namespace std;
class A {virtual void f(){}};
class B {virtual void f(){}};
int main() {
A* pa = new A;
B* pb = new B;
void* pv = dynamic_cast<void*>(pa);
cout << pv << endl;
// pv now points to an object of type A
pv = dynamic_cast<void*>(pb);
cout << pv << endl;
// pv now points to an object of type B
}
dynamic_cast认为空指针的转换安全的,但这里类A和类B必须是多态的,包含虚函数,若不是,则会编译报错。
3.3.2、自定义转换函数(成员函数)
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
A(int data = 1):a(data){}
operator int() const{//无返回
return a;
}
private:
int a;
};
/*类外定义成员函数*/
//A::operator int() const{
// return a;
//}
int main(void)
{
A a(10);
int b = a;
cout << b << endl;
return 0;
}
运行结果为:
10
转换函数的调用时间:‘=’赋值符出现的时候,不一定在变量定义的时候。
3.3.3、隐式转换(explicit)
在构造函数前加explicit,可以防止隐式调用构造函数。
#include <stdio.h>
#include <iostream>
using namespace std;
class A{
public:
explicit A(int data):a(data){}
private:
int a;
};
int main(void)
{
A a(100);//编译通过
A a1 = 100;//编译不通过,因为这是隐式调用构造函数
return 0;
}
四、模板
4.1、类型模板
4.1.1、模板函数
#include <stdio.h>
#include <iostream>
using namespace std;
template <typename T>//或者template <class T>
T add(T a, T b)
{
return a+b;
}
int main(void)
{
cout << add(1,1) << endl;
cout << add(1.1, 2.2) << endl;
return 0;
}
运行结果为:
2
3.3
编译器编译的时候,如果调用add的时候发现形参的类型为两个整型,那么就会依据这个模板编写出一个函数,如果发现还调用了形参为两个浮点型的add函数,那么会根据这个模板编写出一个浮点型的函数,会生成真实的函数对象。
模板的好处在于:不需要写两个功能相似的函数,只需要写一个模板即可,减轻用户的工作量。
4.1.2、模板类
#include <stdio.h>
#include <iostream>
using namespace std;
template <typename xxx>
class ARR{
public:
ARR():tail(0){}
void addtail(xxx value);
void show(void);
private:
xxx data[100];
int tail;
};
template <typename xxx>
void ARR<xxx>::addtail(xxx value)
{
this->data[tail++] = value;
}
template <typename xxx>
void ARR<xxx>::show(void)
{
int i = 0;
for(i = 0; i < tail; i++){
cout << data[i] << " , ";
}
cout << endl;
}
int main(void)
{
ARR<int> arr_int;
ARR<float> arr_float;
arr_int.addtail(1);
arr_int.addtail(2);
arr_int.show();
arr_float.addtail(1.1);
arr_float.addtail(2.2);
arr_float.show();
return 0;
}
运行结果为:
1 , 2 ,
1.1 , 2.2 ,
注意:
1、一个template <typename xxx> 只作用一个函数或一个类。
2、模板类声明时,不允许将类声明与类外定义的成员函数写在不同的文件中,不然会出现链接错误。
3、模板函数和模板类在使用中有差别,模板类要加<>。
4.2、非类型模板以及特化
4.2.1、非类型模板
非类型模板:类型模板只是数据类型不确定,而非类型模板数据在前面的基础上,数据大小也不确定。
#include <stdio.h>
#include <iostream>
using namespace std;
template <typename T, int SIZE>//注意:SIZE在使用时为常量
class ARR{
public:
ARR(){};
~ARR(){}
void size(void){
cout << sizeof(arr) << endl;
}
private:
T arr[SIZE];
int num;
};
int main(void)
{
ARR<int, 10> arr_int;
//const int a = 10;
//ARR<int, a> arr_int;
arr_int.size();
return 0;
}
运行结果为:
40
4.2.2、特化
应用场景:在实现时明明可以套用模板,但是不希望套模板,此时就需要特化。
编译器优先使用特化的。
#include <stdio.h>
#include <iostream>
using namespace std;
template <typename T>
T add(T a, T b)
{
return a+b;
}
bool add(bool a, bool b)
{
if(a == true && b == true)
return true;
else
return false;
}
int main(void)
{
cout << add(1,0) << endl;
cout << add(true, false) << endl;//程序中有明确写好的函数,系统优先调用
return 0;
}
运行结果为:
1
0
4.2.3、偏特化
注意:这个类名要与模板的类名要相同!!!
#include <vector>
#include <stdio.h>
#include <iostream>
using namespace std;
template <typename T1, typename T2>
class A{
public:
T1 t1;
T2 t2;
};
template <typename T1>
class A<int,T1>//注意:这个类名要与上面的类名相同!!!
{
public:
int t;
T1 t1;
};
int main(void)
{
A<int, int> a;
A<int, float> b;//虽然偏特化的类上,在定义时已经说明,当时在定义对象时还是要注明int
}
若有理解出错的地方,望不吝指正。
五、参考文献
[1]: C++ 引用基本用法
[2]: C++异常处理(try catch throw)完全攻略
[3]: c++中的四种类型转化详解