学好C++,走遍天下都不怕
在开始我的DirectX11 旅程之前,我想先回顾一下被我丢掉的C++,我想在这一篇里解决它。算是对C++ Primer Plus 这本书的总结提炼。可能篇幅较长~~
当我了解了C语言的结构化和自顶向下的面向过程编程之后,我就会感觉头大,因为它注重的是每个事件,比如说你早晨起床、洗脸、刷牙、出门,你需要做的是一步一步的去实现这个过程,顺序不能乱,而面向对象呢,它注重的是你这类人,起床、洗脸、刷牙、出门都是你的一个行为,你只需要写出这些行为的方法,具体执行交给某个人去执行,执行顺序也不一定,对比起来,你就可以发现面向对象的优越性。
封装
继承
多态
泛型编程
,这四点可以作为C++的四大特性
。这里简要介绍一下:特性 | 特点 |
---|---|
封装 | 是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。简而言之,把实现过程和数据包围以来,只留下接口给你访问。 |
继承 | 一指一个对象从另一个对象获得功能的过程。比如,鸟类具有飞翔的能力,而喜鹊属于鸟类,它可以继承鸟类飞翔的方法,而不需要重新实现。 |
多态 | 指同一名字的事物可以完成不同的功能。具体后面细说。 |
泛型编程 | 它与OOP的目标相同,即使重用代码和抽象通用概念更简单,但OOP强调编程的数据方面,而泛型强调的是独立于特定数据类型。 |
首先,实现“hello,world
”。
// first.cpp
//头文件
#include
#include
//命名空间
using namespace std;
//主函数
int main()
{
cout<<"hello,world";
return 0;
}
简单易懂。需要注意的有:
cin>>
和cout<<
,如果想使用C的printf()
和scanf()
,只需要包含stdio.h
的头文件。.h
结尾,但现在C++可以没有扩展名,C加上前缀c,例如cmath
。新式风格要加命名空间。main()
函数名称应该全部英文小写,而且必不可少(少数特殊情况例外)
OOP
的本质是设计并扩展自己的数据类型。
存储整数的类型,依次是(按从小到大):
bool、char、signed char、unsigned char、short、unsigned short,int,unsigned int、long、unsigned long
注意:signed char
取值范围是-128
到127(有符号位)
;unsigned char
取值范围是 0 到 255
;unsigned 本身是unsigned int 的缩写,无符号类型可以增大存储最大值,如short 表示范围为-32768到+32767
,无符号版本就是0-65535
。
C++11新增了long long,unsigned long long
还有wchar_t
。
存储浮点数的类型,float,double,long double
。
+
-
*
/
%
++
,还有自减- -
关系运算符
==
!=
>
<
>=
<=
&&
||
!
位运算符
^
&
|
>>
<<
~
赋值运算符
=
+=
-=
/=
*=
%=
<<=
>>=
&=
|=
^=
还有运算符优先级具体查看C++手册
为了处理比简单类型更复杂的数据,需要复合类型的帮助,影响最大就是类,还有几种更简单的类型,来自于C语言。包括数组、结构、指针等。
通用格式为:
typeName
arrayName
[arraySize
]
字符串是存储在内存中的连续字节中的一系列字符。
注意:以数组存储的C风格字符串必须以空字符串/0
结尾,而基于string类库的则不需要,因此第二种更适用,如:
char dog[3]={‘a’,’b’,’/0’} ;
char dog[3]=”ab”;
string类: 使用需要包含头文件string,由于string类在名称空间std中,需要一条using 编译指令,或者使用std::string来引用它。
数组可以存储多个元素,但是这些元素的类型必须相同,比如存储一个人的姓名、身高、体重等信息,使用数据显然不合适,结构可以满足,它比数组更灵活。
比如上面所说的该怎么写?
//struct 关键字
//information 标记为新类型的名称
struct information
{
//结构成员
char name[20];
float tall;
float weigh;
};
定义之后便可以创建这种类型的变量,通过.来访问各个成员
information personone;
// personone.name访问name这个变量
还可以使用赋值运算符(=)进行成员赋值,如下:
//接上面的代码,先初始化一个成员
information personone =
{
"zhangsan";
180.0;
70.0;
};
information persontwo;//定义一个变量two
persontwo=personone;//把one赋值给two
也可以定义结构数组:
information person[2]
{
{"lisi",179.0,70;};
{"wangwu",185.0,75.0};
};
//person[0]就是lisi的信息,person[1]就是wangwu的信息
共同体union与是一种数据格式,能够存储不同的数据类型。但只能同时存储其中的一种。结构体可以同时存储int、long、double,但共同体只能存储其中的一种,比如说,管理一个商品的ID,一些ID为整数、一些为字符串,显然你查找一个ID是它只能是整数或者字符串,不可能同时满足。
如下:
union id
{
long id_num;
char id_char[10];
};
id idchoose;
if(idchoose==1)
cin>>idchoose.id_num;
else
cin>>idchoose.id_char;
当然其可以作为struct类型的子类型:
struct information
{
//结构成员
char name[20];
float tall;
float weigh;
union id
{
long id_num;
char id_char[10];
} id_val;
};
information personthree;
//访问 personthree.id_val.id_num
当然可以不需要中间标识符id_val,此时共同体为匿名共同体,如下:
struct information
{
//结构成员
char name[20];
float tall;
float weigh;
union
{
long id_num;
char id_char[10];
} ;
};
information personthree;
//访问 personthree.id_num
共同体常用于(不限于)节省内存。
数学中我们就学过枚举的含义,C++使用枚举来定义符号常量,可以代替const,如下:
//colorchoose是类似于struct的结构体变量,
//red, green,yellow是符号常量,对应整数0-2
enum colorchoose {red, green, yellow}
colorchoose band;
band=green;
设置枚举量的值
//显式设置
enum num{first=2, second=4,third=8}
//隐式设置
enum num{first, second=20,third}
//此时first=0,third=21;
学得最头疼的就是指针了,在这里深刻剖析一下指针。在了解指针之前,先了解两个概念:
程序编译时
:编译器帮你把源代码翻译成机器能识别的代码。
程序运行时
:就是代码跑起来了.被装载到内存中去了.(你的代码保存在磁盘上没装入内存之前是个死家伙.只有跑到内存中才变成活的)
详细请访问这里
什么是指针?
指针是一种特殊类型的变量,用于存放值的地址。指针名表示的是地址,而
*
运算符被称为间接值,将其应用于指针,会得到该地址处存储的值。举个例子:
int data=3;
int * p_data;
p_data= &data;
p_data表示的是一个地址,*data表示存储在这个地址的值,它是int类型的。
&是取址运算符,&data是取存储值3的地址,赋予p_data。由于p_data的地址指向了3的地址, *p_data=data=3。
如何声明初始化指针?
还是以上面的为例:
int * p_data;
由于p_data处的值类型为int,int* 定义的必须是指针。
*的位置可以不固定,可以不要空格,都不重要。
但是 int * p1, p2;声明表示创建了一个指针p1和一个int 变量p2。所以每个指针都需要一个*
指针是地址,在计算机内存中一般都是固定的,对指针加减乘除操作是没有意义的。
使用指针实现在程序运行时分配内存
前面我们在初始化的时候将指针变成变量的地址,变量在编译时分配有名称的内存。而指针的真正作用在于:在运行阶段分配未命名的内存以存储值。在C语言中使用malloc()来分配内存,C++有更好的方法——new运算符。
通用格式为:
typeName * pointer_name = new typeName
两个地方定义数据类型。含义是:程序员告诉new, 需要为哪种数据类型分配内存,new找到一块正确的内存块,并返回其地址。接下来,将地址赋予pointer_name,* pointer_name表示存储在那里的值。
需要说明的是,使用new分配的内存块通常存储在堆或者自由存储区中,而使用变量声明分配的内存通常在栈中。
C++提供了检测并处理内存分配失败的工具。
使用delete来释放内存
例如:
int *pn =new int;
……
delete pn;
需要说明的是:
指针数组
前面说道使用new来为变量分配内存,但是对于大型数据使用new才是它的用武之地。例如,如果使用声明来创建数组,程序在编译时将为它分配内存,不管程序是否使用这个数据,它都占了内存。这叫做静态联编,在编写程序时必须确定数组的长度。但使用new来创建,在运行时如果需要则创建,不需要就不用创建。这种数组叫做动态数组,程序会在运行时确定数组的长度。
通用格式是:
typeName * pointer_name = new typeName [nam_elements];
释放则是 delete []
如果创建的是实体,不需要[ ]
//创建一个动态数组
int *pn =new int [10];
动态数组创建后注意:
如果pn=pn+1, 那么会发生什么呢?前面说过,pn是指针的名称,pn+1之后,pn[0]指的是数组中第二个元素而不是第一个。
数组的地址:例如定义一个数组
short num[4];
cout<cout<<&num<
输出会是什么呢?分别是num[0]和所有数组元素地址。
根据不同的分配存储的方法,C++有3种管理内存的方式:分别是静态存储、自动存储、动态存储。这是计算机组成原理的内容,具体请访问:内存管理
模板类vector和array是数组的替代品,如何替代呢?有何优点?
模板类vector
类似于string 类,它也是动态数组,可以在运行阶段设置 vector对象的长度,末尾添加新数据等,还可以在中间添加新数据。基本上是new创建动态数组的替代品。格式为:
vector
vt(n);
参数n可以是整形常量,可以是整型变量。
模板类array
array对象和数组一样,长度固定,但不同的是更方面、更安全。
格式为:
array
arr
;
对比:
除了存储数据,计算机还可以对数据进行各种操作,比如修改、合成、重组等,C++提供了操作工具。
1. for循环
:
for(int i=0,i<5,i++)
{
cout<<"hello,world";
}
基于范围的for循环
double prices[5]={5.23, 6.25, 5.32, 7.25, 4.25};
for (double x:prices)
cout<std:endl;
2. while循环
while(condition)
body
就是说先判断条件,true去执行循环体内的内容,直到false结束。
3. do while 循环
do
body
while(condition);
解释一下就是先执行循环体,判断条件,true继续执行,false终止。
1. if语句
2. if else语句
3. if else if else语句
4. ?:条件运算符
expression1 ? expression2 : expression3
//如下意思是如果5>3是true,结果是10,否则是12
5>3 ? 10 :12
5. switch语句
cin>>choice;
switch(choice)
{
case 1 :cout<<"zhangsan";
break;
case 2 :cout<<"lisi";
break;
default: cout<<"end";
}
意思就是如果输入choice是1,输出zhangsan;2输出lisi,否则输出end;
6. break和continue语句
break是直接跳出循环,去执行循环体外的代码;continue是跳出循环体中余下的代码,开启新一轮循环。
从函数开始,就涉及到了C++的编程模块,我们可以把前面所学运用到这一模块,来实现某种特定功能。
函数原型:在ANSI C新标准中,允许采用函数原型方式对被调用函数进行说明,其一般格式如下:
返回值类型 函数名(参数表)
什么时候使用函数原型?
当被调用函数的定义出现在主调用函数(一般是main函数)之后时,应在调用语句之前给出函数原型。如果在被调用之前,没有给出函数原型,编译器将会把第一次遇到的该函数定义作为函数的声明,并将函数返回值类型默认为 int 型。那么当函数返回值类型为整型时,就无须给出原型了呢?那你错了,这种偷懒的方法将使得编译器无法对实参和形参进行匹配检查。若调用函数时参数使用不当,编译器也不会再给你善意的提醒,你也许会得意于程序的安全通过,但你很可能将面临类型不匹配所带来的系统崩溃的危险。
函数参数和按值传递
side=5;
double volume=cube(side);
double cube(double x)
{
return x*x*x;
}
如上所示:side首先赋予cube()函数,然后将值传递给x,注意:此时cube()使用的是double变量,而不是原来的数据。前面的用于接收传递值的变量叫做形参,而传递给函数的值被称为实参。C++使用参数表示实参、参量表示形参。
#include
const int ArSize=8;
//函数原型声明
int sum_arr(int arr[],int n)
int main()
{
using namespace std;
int cookies[ArSize]={1,2,4,8,16,32,64,128};
int sum=sum_arr(cookies,ArSize);
cout<int sum_arr(int arr[],int n)
{
int total=0;
for(int i=0;ireturn total;
}
大多数情况下,C++和C将数组名视为指针,所以cookies== &cookies[0],所以函数传递的是地址。所以int arr[ ]可以使用int *arr来代替,但是当且仅当用于函数头或者函数原型中。
使用数组名与指针对应有什么好处?
将数组地址作为参数可以节省复制整个数组所需的时间和内存,而且使用原始数据增加了破坏数据的风险,可以使用const保护数组。
const 用于指针
两种方式:
int age=20;
const int *pt = &age;
此时 *pt +=1是错误的。虽然我们无法用指针来修改指向的值,但是我们可以直接修改age这个值。
const int age=20;
const int *pt = &age;
此时直接修改age和使用指针修改都不被允许。
const int age=20;
const int *pt = &age;
C++ 不允许这样使用,因为 pt指针可以修改age,但是age是const状态,无法被修改,相互矛盾。
函数在结构里的运用和数组并没有太大的差别,与数组相比,我们是把结构作为完整的实体进行传递,你需要注意的是结构体定义的是一种特殊的变量,函数返回的值的类型是你定义的变量类型。
与数据项相似,函数也有地址,函数的地址是存储其机器语言代码的内存的开始地址。对我们来说,这些并没有什么用,但对程序来说却很有用。例如,将一个函数地址作为另一个函数的参数,这样就可以让第一个函数找到第二个函数,虽然与直接调用函数相比,方法很笨拙,但允许在不同的时间传递不同函数的地址,意味着可以在不同的时间使用不同的函数。
内联函数与常规函数的区别在于编译过程:常规函数在执行时编译执行时,计算机会把他们编译成机器语言指令,在执行过程中,比如函数调用过程中,程序会跳到被调用的函数,执行完成之后再跳回来。而内联函数使用相应的函数代码来替换函数调用,也就是说,如果程序有十个地方调用函数,该程序将包含该函数的十个副本。
因此,好处是可能会节省时间,但如果函数体很大,就不适合使用内联技术。而且内联函数不能递归。所以可以有选择的使用它。
使用方法是在函数声明或定义前加上关键字 inline
引用是已经定义变量的别名,主要作用是用作函数的形参。将引用变量用作参数,函数将使用原始数据,而不是副本,前面说过,应该避免直接使用原始数据。
创建引用变量:
int i;
int & j= i;
此时,&不是取地址,而是声明引用。
声明引用时必须将其初始化,而不是像指针那样,先声明再赋值。
将引用作为函数参数和指针是一样的效果,可以避免按值传递的限制。
默认参数是指当函数调用中省略实参自动使用的一个值,必须从右向左添加默认值:
int apple(int n,int m=4,int j=5); //valid
int banana(int n,int m=4,int j); //invalid
int watermelon(int k=1,int j=2,int z=3); //valid
apple()原型允许调用函数提供一个、两个、三个参数
apples= apple(3); //apples(3,4,5)
apples= apple(2,9); //apples(2,9,5)
C++ 规定,在给定的作用域中只能指定一次默认参数。
一般来说,只需要在声明的时候添加默认参数,如果是两个C++文件的函数调用,可以在声明和定义函数时分别指定参数。
简单来说,函数重载就是使用相同名称的函数来实现不同的功能,当然,函数名可以一样,但参数列表不同。
注意,引用类型与类型本身是一致的。
eg.
char sink(double r);
char sink(double &r); //与上面是相同的
char sink(double r,int k);
什么情况下使用函数重载?
仅当函数基本执行相同的任务,但使用不同形式的数据,才使用。
函数模板简单来说就是使用泛型来定义函数,泛型可用具体的类型替换。简单例子:
#include
template <typename T>
void Swap(T &a,T &b);
int main()
{
int i=10,j=20;
Swap(i,j);
double x=2.2,y=3.2;
Swap(x,y);
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp=a;
a=b;
b=temp;
}
//可以看出第一次 T 被替换成int ,第二次 T 被替换成double
什么时候使用模板?
如果需要多个将同一种算法用于不同类型的函数。当然可以像重载函数那样重载模板,这样可以实现更多的功能。
管理我们的项目:
想想平时我们是怎么管理C++文件的,我们总是在一个C++文件里实现了所有的功能。这样其实是很不好的,一种有效的组织策略是,使用头文件来定义用户原型,为操纵用户类型提供函数原型;将函数定义放在一个独立的源文件代码中;最后将main()函数和其他使用这些函数放在第三个文件中。
名称空间的使用:
我们在使用类库的时候,如果两个库都有一个List类,我们该怎么办呢?方法就是使用名称空间。名称空间可以是全局的,可以位于另一个名称空间里(可以嵌套),但不能位于代码块中。通过作用域解析运算符::可以限定该名称。
|——————————————————————————————————————————
当然,我们并不想每次都限定,这时我们就可以使用using 声明或者using 编译指令。区别在于:using 声明比如 Jill::fetch 可以特定区域,也可以添加到全局空间,好处在于安全,而using编译指令 using namespace std;使用更加方便,缺点就是不安全,因为std里面可能有很多名称,使得空间很开放。
前面已经说过面向对象的一些特点了,这里说说抽象类和接口。
抽象类:在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象。
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
接口:接口就是一个共享框架,供交互时使用。比如,电脑与打印机之间的交互就是通过接口来实现的,我们开发的程序无论是供人使用还是供其他程序使用都是需要接口来实现的。
在类中,不仅需要定义公有和私有,还可以定义类的属性,还可以定义类的方法,也就是成员函数。
eg.
class Dog
{
private:
int age;
double tall;
void jump();
public:
char name;
void run();
void walk(){speed=time*i};
};
如上:private定义了私有部分,这说明该部分不能被外部访问,可以不写private,因为这是默认私有,其中public 中void都是该类的成员函数,这部分可以作为接口,而private中的实现了数据封装。值得一提的是,类方法可以访问private组件的。
实现类成员函数方法是使用::来表示函数所属的类如:
void Dog::run()
而如果在类声明中已经实现了方法如walk(),该函数自动成为内联函数。
创建了类之后,我们使用构造函数来初始化对象。比如说我定义了Dog这个类,而类中可能有很多Dog对象,我们的操作是对这些对象而非类,所以:
//显示调用构造函数
Dog dog1= Dog(2,0,5,"wangcai");
//隐式调用构造函数
Dog dog2(10,1.01,"dahuang");
这样我创建了一个dog1和dog2对象,并初始化了对象。后面会说到使用new来创造对象。
创建了对象之后,程序会使用它并完成一定的功能,但是对象过期之后呢?程序会调用一个特殊的成员函数来实现清理工作,它就是析构函数。
Dog::~Dog()
{
}
析构函数可以没有返回值和声明类型,也没有参数。
什么时候调用析构函数呢?
这由编译器决定。
所有成员函数包括构造和析构函数都有一个this指针,它指向的调用对象,也就是说this指针是调用对象的地址, * this是该对象的别名。如果方法需要显式调用它的对象,可以使用this指针。
说一个简单的实例来说明什么是运算符重载:某同学早上花了2小时40分钟来学习,下午花了3小时50分钟来学习,问:他一天总共花了多少时间来学习?常规方法是分开计算,因为单位不同。
//mytime0.h --函数名称
//#ifndef x //先测试x是否被宏定义过
#ifndef MYTIME0_H_
//没有,就定义x,并编译程序段
#define MYTIME0_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h,int m=0);
void AddMin(int m);
void AddHours(int h);
void Reset(int h=0,int m=0);
Time Sum(const Time &t) const;
void show() const;
};
//终止if,这样这个.h文件就可以在其他文件中使用了。
#endif
接下来:
//mytime0.cpp --实现类中的成员函数
#include<iostream>
#include "mytime0.h"
//初始化
Time::Time()
{
hours=minutes=0;
}
//赋值
Time::Time(int h,int m)
{
hours=h;
minutes=m;
}
//分钟数计算
void Time::AddMin(int m)
{
minutes+=m;
hours+=minutes/60;
minutes %=60;
}
//小时数计算
void Time::AddHours(int h)
{
hours+=h;
}
//重置操作
void Time::Reset(int h, int m)
{
hours=h;
minutes=m;
}
//在类成员函数的声明和定义中,
// const的函数不能对其数据成员进行修改操作。
// const的对象,不能引用非const的成员函数。
//计算结果
Time time::Sum(const Time & t) const
{
Time sum;
sum.minutes =minutes +t.minuets;
sum.hours=hours +t.hours+sum.minutes/60;
sum.minutes %=60;
return sum;
}
//显示
void Time::show() const
{
std::cout<<hours<<"hours,"<<minutes<<"minutes";
}
最后,来测试它:
//usetime.cpp --主函数
#include
#include "mytime0.h"
using namespace std;
int main()
{
Time morning(2,40);
Time afternoon(3,50);
Time total;
//早上加晚上
total=morning.Sum(afternoon);
total.show();
return 0;
}
这是一个很规范的写法,但这只是常规写法,运算符重载可以实现sum()操作,只要将sum()名称改为operator +()就可以重载加法运算符。比如:
Time operator +(const Time &t)const;
//total就可以这样写:
total=morning.operator+(afternoon);
//也可以这样
total=morning+afternoon;
可以看出运算符重载可以实现用户自定义类型的操作,需要注意的是,运算符不能违反原来的规则,也就是说加法你不能用 -,而且,并非所有运算符都可以重载,具体请查看手册。
前面说到,对于类中的私有部分,只要公有类方法可以访问。但这样太过严格,C++提供了新的访问权限:友元。
友元分为三种:友元函数、友元类、友元成员函数。
创建友元函数:
1. 原型声明前加上关键字 friend;
2. 函数定义不要使用Time::限定符(想想,成员函数还需要友元吗?)。不要使用friend。
总之,类的友元函数是非成员函数,但有成员函数一样的访问权限。
友元类:它的所有方法都可以访问原始类的私有成员和保护成员。
友元成员函数:让特定的类成员成为另一个类的友元,不必让整个类都成为友元。
有时,我们确实需要进行类型转换,比如float转int 等,有时候不经意间定义错了类型,然后类型被转换了这是隐式转换,有时候我们需要显式转换类型。
在类的构造函数中,使用new为数据分配内存,然后将内存地址赋给类成员。但如果对象包含成员指针,同时它指向的内存是由new分配的,则释放内存时不会自动释放对象成员指针指向的内存,因此需要delete来释放分配的内存,这样对象过期后,将自动释放其指针成员指向的内存。如果对象包含指向new分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象的时候,当出现原始对象与被赋值的对象指向对一个数据块,如果我们最终删除这两个对象的时候,会出现对同一个数据块删除两次,新的解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载运算符。这种情况实际是复制了实际的数据,而不仅仅是复制指向数据的指针。
在声明成员时加上static关键字,便可以将该成员设置为静态成员(包含数据成员和函数成员)它有以下特点:
1. 静态数据成员的类型可以是常量、引用、指针、类类型等。
2. 普通数据成员属于类的一个具体的对象,只有对象被创建了,普通数据成员才会被分配内存。而静态数据成员属于整个类,即使没有任何对象创建,类的静态数据成员变量也存在。
3. 因为类的静态数据成员的存在不依赖与于任何类对象的存在,类的静态数据成员应该在代码中被显式地初始化,一般要在类外进行。
4. 静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针,这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。静态成员函数不可以同时声明为 virtual、const、volatile函数。
5. 静态成员函数一般只能访问静态成员变量,如果要访问非静态成员变量的话,只能访问某一个对象的非静态成员变量和静态成员函数。可以传一个对象的指针,引用等参数给这个静态成员函数。
6. 外部访问类的静态成员能直接通过类名来访问。
7. 类的静态成员函数无法直接访问普通数据成员(可以通过对象名间接的访问),而类的任何成员函数都可以访问类的静态数据成员。
面向对象编程主要目的之一是提供可重用的代码,这样会大大减少我们的工作量,通常,我们可以从已有的类派生出新的类,而派生类继承了原有类的特征包括方法,而自身可以对其修改以满足需求。
派生类继承了什么?
派生类对象继承了基类的数据成员和方法。注意,基类的私有部分也是派生类的一部分,但只能通过基类公有和保护方法访问。保护成员在派生类中类似与公有成员,而在类外类似于私有成员。
派生类可以扩展什么?
需要自己的构造函数,析构函数和赋值运算符(不继承),但构造函数必须使用基类的构造函数。可以根据需要添加额外的数据成员和成员函数。
派生类如何创建对象和删除对象?
创建对象:首先调用基类构造函数,然后调用派生类构造函数
删除对象:先调用派生类析构函数,然后调用基类析构函数。
定义虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
虚函数中的使用限制
普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
构造函数不能是虚函数,否则会出现编译错误。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0
可以查看知乎讨论
程序在运行时可能会遇到错误,比如零除操作、试图打开不可用的文件、请求过多内存等等,原始的做法我们可能会在非常靠近函数的地方处理错误代码,比如说零除我们可能使用if…else语句来处理除数为0的情况。当然这样会使代码耦合性太高。
还有就是调用abort()来直接终止程序,或者使用函数的返回值来指出问题。
但这都不是很好的解决方案。
C++异常机制包括三个部分:
引发异常
使用 throw关键字
if(a==-b)
throw "a=-b not allowed"
return 2*a*b/(a+b)
C++中通过catch关键字来捕获异常,捕获异常后可以对异常进行处理,这个处理的语句块称为异常处理器。下面是一个简单的捕获异常的例子:
try{
//do something
throw string("this is exception");
} catch() {
cout << "catch a exception " << endl;
}
try块标识其中特定的异常可能被激活的代码块,这些代码块表示注意这些代码引发的异常,后面跟一个或多个catch块,用来处理异常。
C++标准库给我们提供了一系列的标准异常,这些标准异常都是从exception类派生而来,主要分为两大派生类,一类是 logic_error,另一类则是runtime_error这两个类在stdexcept头文件中,前者主要是描述程序中出现的逻辑错误,例如传递了 无效的参数,后者指的是那些无法预料的事件所造成的错误,例如硬件故障或内存耗尽等。