C++初学知识点总结
1、namespace:
1)定义:
所谓的namespace就是指标识符的各种可见范围,C++标准程序中的所有标识符都被定义于一个名为std的namespace中。
2)iostream与iostream.h的差别
差别当然不只是一个带后缀.h,二者的代码是不一样的,带后缀的c++标准明确提出不支持了,早期的实现将标准库功能定义在全局空间里,声明在带.h的后缀的头文件里,C++标准为了区分,也为了正确使用命名空间,规定头文件不带后缀,因此当使用带后缀.h时相当于c中调用库函数,使用的是全局命名空间,也就是早期的c++实现。例如当使用<iostream>时该头文件没有定义全局空间,必须使用namespace std,这样才能正确使用cout。
因为标准库非常的庞大,所以在选择定义类的名称或函数名的时就很有可能和标准库中的某个名字相同。为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。但这又会带来了一个新问题。无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h> 和<iostream>等等这样的头文件,一个是为了兼容以前的C++代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加".h"
3)使用标准库的标识符的几种选择
1 | 使用指定符-》直接指定如 std::cout<<"hell"<<std::endl; |
2 | 使用默认命名空间中的变量:::variable; 如using std::cout, 那(1)里就可以写成cout<<"hello"<<endl; |
3 | 最方便的就是使用using namespace std; -》使用整个命名空间:using namespace name; |
4)C与C++中的命名空间
C | C++ |
在C语言中只有一个全局作用域;全局标识符共享同一个作用域; | 命名空间将全局作用域分成不同的部分,因此不同命名空间下的标识符可以同名 |
标识符之间可能发生冲突 | 命名空间可以相互嵌套;全局作用域也叫默认命名空间。 |
2、C++的增强
1)C++对比c实用性增强
C语言中的变量都必须在作用域开始的位置定义,C++中更强调语言的“实用性”,所有的变量都可以在需要使用时再定义。
C语言中允许重复定义多个同名的变量,C++中不允许定义多个同名变量,C语言中多个同名全局变量最终会链接到全局数据区的同一个地址空间上。
2)struct类型增强
C中的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型,C++中的struct是一个新的数据类型的定义声明。
C++中所有变量和函数必须有类型,C语言的默认类型在C++中不合法,在C语言中int f( );表示返回值为int,接受任意参数的函数,int f(void);表示返回值为int的无参函数在C++中int f( ); 和int f(void)具有相同的意义,都表示返回值为int的无参函数。C++更加强调类型,任意的程序元素都必须显示指明类型。
3)C++ 的bool
bool | |
C++在C基本类型系统之上增加了bool | 可取的值只有true和false,非布尔值转布尔值,非0为true |
理论上bool只占用一个字节 | 如果多个bool变量定义在一起可能会各占一个bit,这取决于编译器的实现 |
c语言返回变量的值;C++返回变量本身 | C语言三目运算符返回是变量值,不能作为左值使用 |
C++中的三目运算符可以作为左值使用其返回变量本身,但如果三目运算符中可能返回值有一个是常量值则不可以作为左值。如(a<b? 1:b)=30; 当左值的条件:要有内存,C++编译器帮程序员取了一个地址而已 |
4)const增强
C++ const定义常量,意味着只读;C++定义的const是一个真正的常量,在const修饰的常量编译期间就已经确定下来了。 C中定义const是只读变量并不是真正的常量。 const与define的相同, |
const int a; 与int const b; 含义一样代表一个常整形数不可更改。 |
const int *c ; c是一个指向常整形数的指针(所指向的内存数据不能被修改,但是本身可改 |
int * const d; d常指针,指针不能被修改,但是它指向的内存空间可以被修改。 |
const int * const e; 一个指向常整形的常指针,指针和所指的内存空间均不能修改。 |
C++中的const常量类似于宏定义,const int c=5; 等价于 #define c 5 。 但C++与宏定义不同之处在于const常量有编译器处理提供类型检查和作用域检查,宏定义由预处理器处理,单纯的文本替换。
C语言中const变量是只读变量,有自己的存储空间。而C++中可能分配存储空间,也可能不分配存储空间,当const常量为全局,并且需要在其它文件中使用,会分配存储空间,当使用&操作符,取const常量的地址时,会分配存储空间,当const int &a = 10; const修饰引用时,也会分配存储空间。
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
int x = 3;
int y = 4;
int z=5;
const int a=10;
int const b;
//a++; 代表⼀一个常整形数
//b=b+1; 代表⼀一个常整形数
const int *c=&y; //是一个指向常整形数的指针
cout<<*c<<endl; //4
//*c=10; //所指向的内存数据不能被修改
c=&x; //但是本⾝身可以修改
cout<<*c<<endl; //3
int *const p = &x; // 定义指向x的指针q
//p = &y; // 发生错误,指针p的地址不能改变
*p = 10;
cout<<*p<<endl; //10
const int *const e=&z;//e一个指向常整形的常指针
//*e=10; //它所指向的内存空间不能被修改
//e=&x; //指针不能被修改
cout<<*e<<endl; //5
return 0;
}
5) 真正的枚举
C语言中枚举的本质就是整形,枚举变量可以是任意整形赋值,而C++中枚举变量只能被枚举出来的元素初始化。
3、变量、引用
1) 变量/引用的基本概念
变量名/引用 | |
变量名 | 实质上是一段连续存储的空间的别名是一个标号,通过变量来申请并命名存储空间,通过变量的名字可以使用存储空间 |
引用 | 变量是一段内存的引用即别名,引用可以看作一个已定义的变量的别名,引用声明:Type& name =var; |
引用没有定义,是一种关系声明与原类型保持一致且不分配内存与被引用的变量有相同的地址 | |
声明必须初始化,一经声明不可变更,可对引用再次引用,多次引用的结果是某一变量具有多个别名。&符号前有数据类型时是引用,其他是取地址。 | |
引用的意义 | 一些场合可以替代指针具有更好的可读性和实用性,引用所占的大小与指针相同, |
Type& name <===> Type* const name ,C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占的空间大小与指针相同。 | |
当函数返回值为引用时,若返回栈变量不能作为其他引用的初始值(不能作为左值),当返回静态变量或全局变量就可以成为其他引用的初始值。 |
#include <iostream>
using namespace std;
struct T
{
int a;
};
void printfT(T *pT)
{
cout<<pT->a<<endl;
}
//pT是t1的别名,相当于修改了t1
void printfT2(T &pT)
{
pT.a=1;
cout<<pT.a<<endl;
}
//pT和t1的是两个不同的变量
void printfT3(T pT)
{
cout<<pT.a<<endl;
pT.a=2;
}
int get1(){
int x;
x=10;
return x;
}
/*int& get2(){
int x;
x=10;
return x;
}*/
int& get3(){
static int x;
x=10;
return x;
}
int main()
{
int a=10;
int b=11;
int &r=a;
cout<<sizeof(r)<<endl; //4 引用所占大小与指针相同,引用是一个常指针
//Type &name <==>Type* const name
//int &r=b; //不可以改变原因用关系
//float &rr=a; //引用类型不匹配
int &rr=r; //可对引用再引用 a有r和rr两个别名
a=11; //直接修改
cout<<a<<endl; //11
//间接修改 1、定义两个变量 (一个实参一个形参)2、建立关联 实参取地址传给形参
//3、*p形参去间接的修改实参的值
{
int *p = &a;
*p = 12;
cout<<a<<endl; //12 引用在实现上,只不过是把
//间接赋值成立的三个条件的后两步和二为一
}
b=14; //与被引用的变量有相同的地址
cout<<"a= "<<a<<" b="<<b<<endl;
//当对引用进行操作赋值时,编译器帮我们隐藏*操作
// cout<<a其实是cout<<*a;*被编译器隐去
T t1;
t1.a=0;
printfT(&t1); //0
printfT2(t1); //1
printfT3(t1); //1
int a1=0;
int a2=0;
a1=get1();
//a2=get2(); //函数的返回值为函数内部定义变量的引用,但函数在调用完毕后
//函数内部定义的变量空间被释放,无法访问,从而造成的错误
//int &a3=get2(); //将一个引⽤用赋给另一个引⽤用作为初始值//
//由于是栈的引用,不能成为其它引用的初始值(不能作为左值使用
cout<<a1<<endl;
a2=get3(); //10 将一个引用赋给一个变量会有拷⻉贝动作
//理解:编译器类似做了如下隐藏操作,a2=*(getA2())
cout<<a2<<endl;
int &a3=get3(); //由于是静态区域,内存合法,即返回静态变量或全局变量可做其他
//引用的初始值(可做右值也可做左值使用)
cout<<a3<<endl; //10
return 0;
}
2)const 引用
const引用可以防止对象的值被随意修改 | const对象的引用必须是const的,将普通引用绑定到const对象是不合法的,const int &y =x;//常引用是限制变量为只读不能通过y去修改x了 |
const 引用可使用相关类型的对象(常量,非同类型的变量或表达式)初始化。这个是 const 引用与普通引用最大的区别。定义const int &a=2; 与 double x=3.14; const int &b=a; 都是合法的。 | |
总结 | const int & e 相当于 const int * const e; |
普通引用相当于 int *const e; | |
当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名; | |
使用字面量对const引用初始化后,将生成一个只读变量 |
#include <iostream>
using namespace std;
struct Teacher
{ char name[64];
int age;
};
//在被调用函数获取资源
int getTeacher(Teacher **p)
{
Teacher *tmp = NULL;
cout<<"p - "<<p<<endl;
if(p == NULL)
{
return -1;
}
tmp = (Teacher *)malloc(sizeof(Teacher));
if (tmp == NULL)
{
return -2;
}
tmp->age = 3;
//p是实参的地址 *实参的地址去间接的修改实参的值
*p = tmp;
return 0;
}
//指针的引用做函数参数
int getTeacher2(Teacher* &myp)
{
//给myp赋值相当于给main函数中的pT1赋值
myp =(Teacher *)malloc(sizeof(Teacher));
if (myp==NULL)
{
return -1;
}
myp->age=6;
return 0;
}
void FreeTeacher(Teacher *pT1)
{
if(pT1==NULL){
return ;
}
free(pT1);
}
int main()
{
Teacher *pT1=NULL;
//c语言中的二级指针
cout<<"Teacher *pT1=NULL " <<pT1<<endl;
getTeacher(&pT1);
cout<<"Teacher &pT1 " <<&pT1<<endl;
cout<<"age:"<<pT1->age<<endl; //3
FreeTeacher(pT1);
//c++中的引用(指针的引用)
//引用的本质间接赋值后2个条件让c++编译器帮我们程序员做了。
getTeacher2(pT1);
cout<<"age:"<<pT1->age<<endl; //6
FreeTeacher(pT1);
int a=10;
int &x=a;
cout<<"x="<<x<<endl;
int b=10;
const int &y=b;
//y=11; //常引用限制变量为只读不可以通过y进行修改
const int c=1;
//int &z=c; //常引用不可以绑定到const对象
double d=3.14;
const int &w=d;
cout<<"w= "<<w<<endl; //3 const 引用可使用相关类型的对象
//(常量非同类型的变量或表达式)初始化
double val = 3.14;
const int &ref =val;
double & ref2 = val;
cout<<ref<<" -- "<<ref2<<endl; //3 -- 3.14
val = 4.14;
cout<<ref<<" -- "<<ref2<<endl; //3 --4.14
return 0;
}
4、函数
1)inline内联函数
c 语言中有宏函数的概念。宏函数的特点是内嵌到调用代码中去,避免了函数调用的开销。但是由于宏函数的处理发生在预处理阶段,缺失了语法检测和有可能带来的语意差错。C++提供的inline实现真正的内嵌。
内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。 | |
C++编译器直接将函数体插入在函数调用的地方,内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。 | |
内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等) | |
内联函数由编译器处理,直接将编译后的函数体插入调用的地方,宏代码片段由预处理器处理, 进行简单的文本替换,没有任何编译过程。 | |
C++中内联编译的限制:不能存在任何形式的循环语句;不能存在过多的条件判断语句;函数体不能过于庞大;不能对函数进行取址操作;函数内联声明必须在调用语句之前; | |
编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。 |
2)默认参数和占位参数
通常情况下函数在调用时形参从实参那里取得值。对于多次调用函数同一实参时,C++给出了更简单的处理办法。给形参以默认值,这样就不用从实参那里取值了。若你填写参数,使用你填写的,不填写用默认值。
在默认参数规则里,如果默认参数出现,那么右边的都必须有默认参数。
占位参数只有参数类型声明,而没有参数名声明一般情况下,在函数体内部无法使用占位参数。
#include <iostream>
using namespace std;
void myprint(int x=3){
cout<<"x= "<<x<<endl;
}
void myprint1(float a, float b=4, float c=5){
//如果默认参数出现,那么右边的都必须有默认参数
cout<<a*b*c<<endl;
}
void myprint2(int a, int b, int){
//占位参数只有参数类型声明,而没有参数名声明
cout<< a+b<<endl;
}
int main()
{
int a=6;
myprint(); //x= 3
myprint(a); //x= 6
myprint1(1); // 20
myprint1(1,2); //10
myprint1(1,2,3); //6
//myprint2(1,2); //必须把最后⼀一个占位参数补上
//若myprint2(int a, int b, int=0)结合默认,皆可调用
myprint2(1,2,3); //3
return 0;
}
3)函数重载
定义 | 用同一个函数名定义不同的函数,当函数名和不同的参数搭配是函数的含义不同 |
调用准则 | 1)将所有同名函数作为候选者,尝试寻找可行的候选函数 |
2)精确匹配实参,通过默认参数能够匹配实参,通过默认类型转换匹配实参 | |
3)匹配失败 | |
4)最终寻找到的可行候选函数不唯一,则出现二义性,编译失败。 | |
5)无法匹配所有候选者,函数未定义,编译失败。 |
4)重载的底层实现
C++利用 name mangling(倾轧)技术来改函数名,区分参数不同的同名函数。实现原理:用 v c i f l d 表示 void char int float long double 及其引用。void func(char a, int b, double c); //func_cid(char a, int b, double c),一个函数不能即作为重载又做为默认参数的函数,当你少写一个参数时系统无法确认是重载还是默认参数。
总结:1)重载函数在本质上是相互独立的不同函数;2)函数参数类型是不同的;3)函数返回值不能作为函数重载的依据;4)函数重载是由函数名和参数列表决定的。
#include <iostream>
using namespace std;
//C++利用 name mangling(倾轧)技术,来改名函数名,区分参数不同的同
//名函数。实现原理:用 v c i f l d 表示 void char int float long double
//及其引用。
void print(int a) //print_i(int a)
{
cout<<a<<endl;
}
/*void print(int a,int b=0){ //不能既作重载,又作默认参数的函数调用
//会存在二义性编译不回通过。
cout<<a+b;
}*/
void print(double a) //print_d(double a)
{
cout<<a<<endl;
}
int main()
{
//调用准则,1、所有同名函数列为候选,2、尝试寻找可行候选,3、精确匹配实参
//4、通过默认参数匹配,5、通过类型转换匹配,6、匹配失败,7最终找到可行不唯一
//8、无法匹配所有候选者,函数未定义,编译失败
print(1); //print(int)
print(1.1); //print(double)
print('q'); //print(int)
print(1.11f); //print(double)
return 0;
}
5、类与对象
1)类的封装与访问控制
c语言封装的概念:当单一变量无法完成描述需求的时候,结构体可以将多个类型打包成一体,行成新的类型。struct中所有行为和属性都是public的(默认),C++中可以指定行为和属性的访问方式。封装可以做到对内开放数据,对外屏蔽数据和提供接口,达到信息隐蔽的功能。struct和class关键字区别:在用struct定义类时,所有成员的默认属性为public;用class定义类时,所有成员的默认属性为private。
#include <iostream>
using namespace std;
class cir{
public:
double r;
double pi=3;
double area=r*pi;
public:
void print(){
cout<<r<<endl; //2
cout<<pi<<endl; //3
cout<<area<<endl; //6.22447e-317
}
};
int main()
{
cir pi;
cout<<"pi.r "<<pi.r<<endl; //pi.r 2.07485e-317
cout<<"pi address "<<&pi<<endl; //pi address 0x7ffd5abf8320
cout<<"pi.pi "<<pi.pi<<endl; // pi.pi 3
cout<<"pi.pi address "<<&pi.pi<<endl; //pi.pi address 0x7ffd5abf8328
pi.r=2;
cout<<"pi.r "<<pi.r<<endl; //2
cout<<"pi.r address "<<&pi.r<<endl; //pi.r address 0x7ffd5abf8320
cout<<"pi.area address "<<&pi.area<<endl; //pi.area address 0x7ffd5abf8330
cout<<pi.area<<endl; //6.22454e-317
pi.print();
return 0;
}
2)对象的构造与析构
构造函数 | C++提供的一个给对象初始化的方案。C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数 |
构造规则 | 在对象创建时自动调用,完成初始化相关工作。 |
无返回值,与类名同,默认无参,可以重载,可默认参数。 | |
一经实现默认构造函数不复存在。 | |
析构 | C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数。 |
析构规则 | 对象销毁时自动调用。完成销毁的善后工作。 |
无返值与类名同。无参不可以重载与默认参数。析构函数的作用,并不是删除对象而在对象销毁前完成的一些清理工作。 |
#include <iostream>
using namespace std;
class Test{
public:
Test(){
cout<<"Test() a= "<<a<<endl;
}
Test(double temp){
a=temp;
cout<<"Test(int a) a= "<<a<<endl;
}
Test(const Test &temp){
a=temp.a;
cout<<"Test(const Test &temp) a= "<<a<<endl;
}
~Test(){
cout<<"~Test() a= "<<a<<endl;
}
void printT(){
cout<<"printT() a= "<<a<<endl;
}
public:
double a;
};
Test t(){
Test temp(1);
return temp;
}
void test1()
{
t();
}
void test2(){
Test m=t();
//如果⽤用匿名对象初始化另外一个同类型的对象,匿名对象转成有名对象,不会析构掉
cout<<"test2 a"<<endl;
}
void test3(){
Test m1(1);
m1=t();//如果⽤用匿名对象赋值给另外一个同类型的对象,匿名对象被析构
cout<<"test3 a"<<endl;
}
void fun(Test t){
cout <<"func begin"<<endl;
cout<<t.a<<endl;
cout<<"func end"<<endl;
}
int main()
{
//Test t;
//Test t1(2);
//Test t2=t1;
//Test t3(t1);
//t.printT();
//t1.printT();
//t2.printT();
//fun(t1);
//test1();
test2();
//test3();
return 0;
}
3)构造函数的分类及调用
有参、无参与拷贝构造函数。
默认无参构造函数 | 当类中没有定义构造函数时,编译器默认提供一个无参构造函数并且其函数体为空; |
默认拷贝构造函数 | 当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制。 |
拷贝构造:由己存在的对象创建新对象。也就是说新对象不由构造器来构造,而是由拷贝构造器来完成。拷贝构造器的格式是固定的。当类中定义了拷贝构或有参造函数时,c++编译器不会提供无参数构造函数;在定义类时只要你写了构造函数,则必须要用。 | |
系统提供默认的拷贝构造器一经定义不再提供,但系统提供的默认拷贝,构造器是等位拷贝,也就是通常意义上的浅拷贝。如果类中包含的数据元素全部在栈上,浅拷贝也可以满足需求的。但如果堆上的数据,则会发生多次析构行为。 |
#include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:
Rect()
{
p=new int(100);
}
Rect(const Rect& r)
{
w=r.w;
p=new int(100);
*p=*(r.p);
}
~Rect()
{
assert(p!=NULL);
delete p;
}
public:
int w;
int *p;
};
int main()
{
Rect rect1;
cout<<rect1.p<<endl; /如果没有实现深拷贝,p指向地址一样的
Rect rect2(rect1);
cout<<rect2.p<<endl;
return 0;
}
4)构造函数初始化列表
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数。如当A的对象是B类的一个成员的时候,在初始化B对象的时候,无法给B分配空间,因为无法初始化A类对象。
当类成员中含有一个const对象或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。初始化列表中的初始化顺序与声明顺序有关,与前后赋值顺序无关。
#include <iostream>
using namespace std;
class ABCD{
public:
ABCD(int a,int b,int c){
_a=a;
_b=b;
_c=c;
cout<<"ABCD() a "<<_a <<" b "<<_b<<" c "<<_c<<endl;
}
ABCD(int a, int b){
_a=a;
_b=b;
ABCD(a,b,100);
}
~ABCD()
{
cout<<"~ABCD() a "<<_a <<" b "<<_b<<" c "<<_c<<endl;
}
int getA(){
return _c;
}
void setA(int val){
_c=val;
}
private:
int _a;
int _b;
int _c;
};
int main(void)
{
ABCD t1(1,2);
cout<<" main "<<t1.getA()<<endl; //0
return 0;
}
5)对象动态建立和释放new 和delete
在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。
new和delete是运算符,不是函数,因此执行效率高。用new分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。malloc不会调用类的构造函数,而new会调用类的构造函数;Free不会调用类的析构函数,而delete会调用类的析构函数。
6)静态成员变量和成员函数
在 C++中静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
类的静态成员 | 属于类也属于对象,但终归属于类static 成员变量实现了同类对象间信息共享 |
static 成员类外存储, static成员是命名空间属于类的全局变量,存储在data区,求类大小并不包含在内 | |
static成员只能类外初始化。 可以通过类名访问(无对象生成时亦可),也可以通过对象访问。 | |
静态函数 | 静态函数不在于信息共享、数据沟通而在于管理静态数据成员,完成对静态数据成员的封装,且静态成员函数只能访问静态数据成员。原因: 非静态成员函数在调用时this指针被当作参数传进。而静态成员函数属于类而不属于对象没有this指针。 |
#include <iostream>
using namespace std;
class Box{
public:
Box(int l,int w):length(l),with(w){
}
int volume(){
return length*with*height;
}
static int height;
int length;
int with;
};
int Box::height=5; //成员是命名空间属于类的全局变量,存储在data区。只能类外初始化
int main(void)
{
cout<<sizeof(Box)<<endl; //8 static 成员类外存储,求类大小并不包含在内
Box b(1,2);
cout<<sizeof(b)<<endl; //8
cout<<Box::height<<endl; //通过类名访问
cout<<b.height<<endl; //通过对象访问
return 0;
}
7)编译器对属性和方法的处理机制
C++类对象中的成员变量和成员函数是分开存储的;
普通成员变量存储于对象中,与struct变量有相同的内存布局和字节对齐方式;
静态成员变量:存储于全局数据区中
成员函数:存储于代码段中。很多对象共用一块代码,代码如何区分具体对象,如:int getK() const { return k; },代码是如何区分,具体obj1、obj2、obj3对象的k值?C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。(静态成员函数不具有普通函数指向具体对象的指针)如果成员函数的形参和类的属性名字相同通过this指针来解决。
8)全局函数与成员函数
1、把全局函数转化成成员函数,通过this指针隐藏左操作数Test add(Test &t1, Test &t2)===》Test add(Test &t2);2、把成员函数转换成全局函数,多了一个参数 void printAB()===》void printAB(Test *pthis);
9)友元
采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的通信接口。但是有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。
除了友元函数外,还有友元类两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。友元可以是一个函数,该函数被称为友元函数;
友元也可以是一个类,该类被称为友元类。友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字 friend,其格式如下: friend 类型 函数名(形式参数);
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。
友元声明以关键字 friend开始,它只能出现在类定义中。因为友元不是授权类的成员,所以它不受其所在类的声明区域 public private 和 protected 的影响。通常我们选择把所有友元声明组织在一起并放在类头之后。友元不是类成员,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
不过类的访问权限确实在某些应用场合显得有些呆板,从而容忍了友元这一特别语法现象。(1) 友元关系不能被继承。(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类;B的友元,要看在类中是否有相应的声明。(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定 是类A的友元,同样要看类中是否有相应的声明。
10)运算符重载
所谓重载就是重新赋予新的意义,运算符重载是对一个已有的函数赋予新的含义,使之有新功能,因此一个函数名就可以用来代表不同的功能的函数即一名多用。运算符重载的本质是函数重载,格式为: 函数类型 operator 运算符名称(形参表列) { 重载实体; } operator 运算符名称在一起构成了新的函数名。
-重载规则
1)大部分运算符都可以重载不允许用户自己定义新的运算符只能在已有的C++运算符进行重载,C++不能重载的运算符有. ; .* ; :: sizeof前两个运算符不能重载是为了保证访问成员的功能不能被改变,域运算符合, sizeof 运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。
2)重载不能改变运算符的运算对象即操作数的个数;且重载不能改变运算符的优先级;且重载不能改变运算符的结合性;
3)重载运算符的函数不能有默认参数否则就改变了运算符参数的个数,与前面第(2)点矛盾。
4)重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一 个是类对象(或类对象的引用)。
5)用于类对象的运算符一般必须重载,但有两个例外,运算符”=“和运算符”&“不必用户重载。 复制运算符”=“可以用于每一个类对象,可以用它在同类对象之间相互赋值。因为系统已为每一个新声明的类重载了一个赋值运算符,它的作用是逐个复制类中的数据成员地址。运算符&也不必重载,它能返回类对象在内存中的起始地址。
(6) 应当使重载运算符的功能类似于该运算符作用于标准类型数据时候时所实现的功能。例如,我们会去重载”+“以实现对象的相加,而不会去重载”+“以实现对象相减的功能,因为这样不符合我们对”+“原来的认知。
(7)运算符重载函数可以是类的成员函数,也可以是类的友元函数,还可以是既非类的成员函数也不是友元函数的普通函数。
6、 继承和派生
类与类之间的关系:1)has-A:包含关系,用一个类由多个“零部件”构成,实现has-A关系用类成员表示,即一个类中的数据成员是另一个类已定义的类。2)uses-A:一个类部分地使用另一个类,通过类之间成员函数的相互联系定义友元或者对象参数传递实现。3)is-A:机制为继承,关系具有传递性不具有对称性。
1)继承和派生关系
类的继承是新的类从已有的类那里得到已有的特性,或从已有的类产生新类的过程就是类的派生,原有的类叫做基类或者父类,产生的新类叫派生类或者子类。派生和继承是同一种意义的两种称谓,isA的关系。派生类中的成员包含两部分,一类是从父类中继承过来的体现其共性,一类是自己增加的成员,体现其个性。
继承特点:全盘接收,除了构造器和析构器,基类可能会造成派生类成员冗余,因此基类是需要设计的,继承方式 class 派生类: [继承方式] 基类名 { 派生类成员声明; } 。 一个派生类可以有多个基类称之为多重继承,派生类只有一个称之为单继承。
2)派生类成员的标识和访问
公有继承:公有继承中基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问,派生类的其他成员可以直接访问他们,无论派生类的成员还是派生类的对象无法访问基类私有成员;
私有继承:当以私有继承的方式实现时,基类中的公有和保护成员都以私有成员身份出现在派生类中,派生类的其他成员可以直接访问他们,但在类外部通过派生类的对象无法访问,而基类中的私有成员在派生类中无论是派生类成员还是通过派生类的对象都不可访问从基类派生而来的私有成员;
保护继承:保护继承基类的公有和私有成员都以保护成员的身份出现在派生类中,派生类的其他成员可以直接访问,但是通过类外部对象无法访问,无论派生类的成员还是派生类的对象都不可以访基类的私有成员。(private成员在子类中依然存在只是无法访问)。
#include <iostream>
using namespace std;
class Base{
public:
int pub;
protected:
int pro;
private:
int pri;
public:
Base(){
pub=0;
pro=0;
pri=0;
}
void set(int a,int b,int c){
pub=a;
pro=b;
pri=c;
cout<<"struct a "<<pub<<" b "<<pro<<" c "<<pri<<endl;
}
};
class son:public Base{
public:
void fun(){
pub=10;
pro=11;
//pri=12; //error
}
};
class son1:protected Base{
public:
void fun(){
pub=10;
pro=11;
//pri=12;
}
};
class son2:private Base{
public:
void fun(){
pub=10;
pro=11;
//pri=12;
}
};
int main()
{
Base b;
b.pub=1;
//b.pro=2; //error
//b.pri=3; //error
b.set(1,2,3);
son s;
s.pub=0;
//s.pro=2;
//s.pri=3;
s.fun();
s.set(1,2,9);
son1 s1;
//s1.pub=1;
//s1.pro=2;
//s1.pri=3;
//s1.set(1,2,3);
son2 s2;
//s2.pub=1;
//s2.pro=2;
//s2.pri=3;
//s2.set(1,2,3);
return 0;
}
3)继承中的构造与析构
类型兼容型原则:是指在需要基类对象的任何地方都可以用共有派生的对象来替代,通过共有继承,派生类得到基类中除构造和析构之外的所有成员,这样公有派生实际就具备了和基类所有相同的功能,像这样:1)子类对象就可以当作父类对象使用,2)子类对象可以直接赋值给父类对象。3)子类对象可以直接初始化父类对象;4)父类指针可以直接指向子类对象;5)父类引用可以直接引用子类对象。在替代完成后,派生类可以作为基类使用但是只能使用从基类继承的成员。(子类就是特殊的父类)
4)继承中的对象模型
类在C++编译器内部可以理解为结构体,子类是是由父类成员叠加子类新成员得到的。继承中的构造与析构调用规则:1)子类对象初始化时会先调用父类的构造函数;2)父类构造函数执行结束执行子类构造函数;3)当父类构造函数有参数时需要在子类初始化列表中显示调用;4)析构函数的调用顺序与构造函数相反。先构造自己再构造成员变量最后自己;析构是先自己再成员变量最后析构父类。
#include <iostream>
using namespace std;
class Base{
public:
Base(){};
Base(const char* s){
this->s=s;
cout<<"Base() "<<" s "<<s<<endl;
}
~Base(){
cout<<"~base() " <<endl;
}
void print(){
cout<<"base "<<endl;
}
private:
const char *s;
};
class son:public Base{
public:
son(){}
son(int a):Base("base from son"){
cout<<"child() "<<endl;
this->a=a;
}
son(int a, const char *s):Base(s){
cout<<"son"<<endl;
this->a=a;
}
~son(){
cout<<"~son"<<endl;
}
public:
void fun(){
cout<<"son "<<endl;
}
private:
int a;
};
void print0(Base *b)
{
b->print();
}
void print1(Base &b){
b.print();
}
int main()
{
son s;
s.fun(); //son
s.print(); //base
Base *b=NULL;
b=&s;
b->print(); //base
//b->fun();
son s1;
Base b1;
print0(&b1); //base
print0(&s1); //base
print1(b1); //base
print1(s1); //base
son s2;
Base b2=s2;
son s3(10);//子类对象在创建时会首先调用父类的构造函数
son so(10,"ps");//父类构造函数执行结束后,执行子类的构造函数当父类的构造函数
//有参数时,需要在子类的初始化列表中显示调用,析构函数调用的先后顺序与构造函数相反
return 0;
}
5)继承中同名成员处理方法
当子类成员变量与父类成员变量同名时子类依然从父类中继承同名成员;在子类中通过域分辨符进行同名成员区分;同名成员存储位置在不同位置;派生类中static关键字:基类中的静态成员将被所有派生类共享,派生类访问成员方式是 类名::成员。或对象名.成员。
#include <iostream>
using namespace std;
class Base{
public:
int a,b;
static int i;
static void Add(){i++;}
void fun(){
cout<<"Base func"<<endl;
cout<<"base statc "<<i<<endl;
}
};
int Base::i=0;
class son:public Base{
public:
int b,c;
void fun(){
cout<<"son fun"<<endl;
i=3;
cout<<"son static i "<<i<<endl;
Base::i++;
Base::Add();
}
};
int main()
{
son s;
s.a=1;
s.b=2;
s.Base::b=3;
s.c=4;
cout<<"s.b= "<<s.b<<endl;//同名成员变量
s.fun();
s.Base::fun(); //同名成员函数通过作用域分辨符进行区分
cout<<"base:: i "<<Base::i<<" son i "<<s.i<<endl;
return 0;
}
6)多继承
一个类有多个直接基类的继承关系成为多继承,派生类::派生类名(总参数表):基类名 1(参数表1),基类名(参数表 2)...基类名 n(参数表n),内嵌子对象1(参数1),内嵌子对象2(参数2)...内嵌子对象n(参数表n){派生类新增成员初始化语句;};
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时可能产生二义性;
继承的公共路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类产生多个基类子对象,要使基类在派生类只产生一个对象必须对这个基类声明为虚继承,是基类成为虚基类(virtual)。
#include <iostream>
using namespace std;
class B{
public:
int b;
B(){
cout<<"Base () "<<endl;
}
void fun(){
cout<<"base i "<<b<<endl;
}
};
class B1:public B{
public:
B1(){
cout<<"B1 ()"<<endl;
}
private: int b1;
};
class B2:public B{
public:
B2(){
cout<<"B2 ()"<<endl;
}
private: int b2;
};
class C:public B1, public B2
{
public:
int f();
private:
int d;
};
int main()
{
C s;
//s.b=10;
//s.B::b=1;
s.B1::b=0;
s.B2::b=9;
return 0;
}
7)多态
如果有几个相似却不完全相同的对象,向他们发出同一消息时他们有不同的反应执行不同的操作,这就叫多态。C++中所谓的多态是指由继承产生的相关不同的类,其对象对同一消息做出的不同响应。
多态实现的前提:赋值兼容规则->指的是需要基类对象的任何地方都可以使用共有派生类的对象来代替。在替代后派生类可以作为基类的对象使用,但是只能使用从基类继承的成员。多态实现的条件:1)要有继承;2)要有虚函数重写;3)要有父类指针(父类引用)指向子类对象。
#include <iostream>
using namespace std;
class B{
public:
B(int b){
this->b=b;
cout<<"Base () "<<b<<endl;
}
void fun(){
cout<<"fun base i "<<b<<endl;
}
virtual void fun1(){
cout<<"virtual Base fun1 "<<endl;
}
private:
int b;
};
class B1:public B{
public:
B1(int b):B(10){
this->b=b;
cout<<"B1 () "<<b<<endl;
}
void fun(){
cout<<"fun b1 "<<b<<endl;
}
virtual void fun1(){
cout<<"virtual B1 fun1 "<<endl;
}
private:
int b;
};
void print0(B *base){
base->fun();
}
void print1(B &base){
base.fun();
}
int main()
{
B *base=NULL;
B p1(20);//Base () 20
B1 b1(30); //Base () 10 B1 () 30
base=&p1;
base->fun(); //fun base i 20
base->fun1(); //virtual Base fun1
base =&b1;
base->fun(); //fun base i 10
base->fun1(); //virtual B1 fun1 多态成立要有继承\要有虚函数重写
//要有父类指针(父类引用)指向子类对象
b1.fun(); //fun b1 30
b1.fun1();//virtual B1 fun1
B &b2=p1;
b2.fun();//fun base i 20
B &b3=b1;
b3.fun();//fun base i 10
print0(&p1); //fun base i 20
print0(&b1); //fun base i 10
print1(p1); //fun base i 20
print1(b1); //fun base i 10
return 0;
}
8)静态联编和动态联编
联编是指一个程序模块、代码之间互相关联的过程。
静态联编(static binding):是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
动态联编:指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch语句和 if 语句是动态联编的例子。
C++与C相同是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。多态的发生是动态联编,是在程序执行的时候判断具体父类指针应该调用的方法。
9)虚析构函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。析构函数可以是虚的。虚析构函数用于指引delete运算符正确析构动态对象
多态的实现原理:当基类中声明虚函数,编译器会在类中生成一个虚函数表。虚函数表是一个存储类成员函数指针的数据结构,表由编译器自动生成和维护,虚函数成员会被编译器放到虚函数表中,存在虚函数时每个对象都有一个指向虚函数表的指针(vptr指针)。
#include <iostream>
#include<string.h>
using namespace std;
class A{
public:
A(){
p=new char[20];
strcpy(p,"obja");
cout<<"Base A "<<endl;
}
virtual ~A(){
delete [] p;
cout<<" ~A "<<endl;
}
private:
char *p;
};
class B:public A{
public:
public:
B(){
p=new char[20];
strcpy(p,"objb");
cout<<"son B "<<endl;
}
virtual ~B(){
delete [] p;
cout<<" ~B "<<endl;
}
private:
char *p;
};
class C:public B{
public:
C(){
p=new char[20];
strcpy(p,"objc");
cout<<"son C "<<endl;
}
virtual ~C(){
delete [] p;
cout<<" ~C "<<endl;
}
private:
char *p;
};
//通过父类指针把所有的子类对象的析构函数都执行一遍释放所有的子类资源
void deletep(A *base){
delete base;
}
int main()
{
C *myC=new C; //Base A son B son C
//delete myC; //~C ~B ~A
deletep(myC); //无virtual -A 。有~C ~B ~A
return 0;
}
10)重载
重载 | 相同的范围(在同一个类中);函数名字相同;参数不同;virtual关键字可有可无。 |
重写(覆盖) | 是指派生类函数覆盖基类函数,特征是:1)不同的范围,分别位于基类和派生类中;2)函数的名字相同;3)参数相同;4)基类函数必须有virtual关键字。 |
重定义 | 重定义(隐藏) 是指派生类的函数屏蔽了与其同名的基类函数 |
规则如下:1)如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。 2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时基类的函数被隐藏。 |
11)纯虚函数与抽象类
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。
定义 | virtual 类型 函数名(参数表) = 0;一个具有纯虚函数的基类称为抽象类 |
性质 | 含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在的意义就是被继承,提供族类的公共接口。 |
纯虚函数只有声明, 没有实现,被“初始化”为 0 | |
如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数,派生类仍然为纯虚基类。 |
绝大多数面向对象语言都不支持多继承,绝大多数面向对象语言都支持接口的概念,C++中没有接口的概念,C++中可以使用纯虚函数实现接口,接口类中只有函数原型定义,没有任何数据的定义。
7、泛型编程
泛型(Generic Programming)即是指具有在多种数据类型上皆可操作的含意。泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。
泛型编程最初诞生于C++中,目的是为了实现C++的STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
#include <iostream>
using namespace std;
int Max(int a,int b){
cout<<" int max(int a,int b)"<<endl;
return a>b?a:b;
}
template<typename T>
T Max(T a, T b){
cout<<" T amx(T a, T b)"<<endl;
return a>b?a:b;
}
template <typename T>
T Max(T a, T b,T c)
{
cout<<"T Max(T a,T b,T c)"<<endl;
return max(max(a,b),c);
}
int main()
{
int a=1,b=2;
cout<<Max(a,b)<<endl;//当函数模板和普通函数都符合调用时,优先选择普通函数
cout<<Max<>(a,b)<<endl; //显示使用函数模板
cout<<Max(3.0,4.0)<<endl; //模板函数模板产生更好的匹配使用函数模板
cout<<Max(5.0,6.0,7.0)<<endl; //重载
cout<<Max('a',100)<<endl;//调用普通函数可以隐式类型转换
return 0;
}
1)函数模版
函数模版的调用是先将模版实化为函数然后调用,函数模版只适用于函数参数相同而类型不同,且函数体相同的情况。如果个数不同则不能使用函数模版。普通函数会提供隐士的数据类型转换,模版不提供,必须严格匹配。当普通函数和模版都匹配是优先选择普通函数。若显示使用函数模版则使用<>类型列表。
编译器的编译过程:编译器并不是把函数模版处理成能够处理任意类的函数,而是编译器从函数模板通过具体类型产生不同的函数,编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
2)类模板
类模板与函数模板的定义和使用类似,有两个或多个类,其功能是相同的仅仅是数据类型不同,所以将类中的类型进行泛化。
模板类派生普通类:子类从模板类继承的时候,需要让编译器知道父类的数据类型具体是什么(数据类型的本质:固定大小内存块的别名)A<int> 。
3)类型转换
类型转换有c风格的,当然还有 c++风格的。c风格的转换的格式(TYPE) EXPRESSION,但是 c 风格的类型转换有不少的缺点,有的时候用 c 风格的转换是不合适的,因为它可以在任意类型之间转换,比如你可以把一个指向const 对象的指针转换成指向非 const 对象的指针,把一个指向基类对象的指针转换成指向一个派生类对象的指针,这两种转换之间的差别是巨大的,但是传统的 c 语言风格的类型转换没有区分这些。还有一个缺点就是c 风格的转换不容易查找,他由一个括号加上一个标识符组成, 而这样的东西在 c++程序里一大堆。
所以 c++为了克服这些缺点,引进了 4个新的类型转换操作符。static_cast 静态类型转换; reinterpreter_cast 重新解释类型转换。dynamic_cast 子类和父类之间的多态类型转换。const_cast 去掉const属性转换。const_cast<目标类型>(标识符),目标类类型只能是指针或引用。
8、异常
异常是一种程序控制机制,与函数机制独立和互补。函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈。
异常设计目的:栈机制是一种高度节律性控制机制,面向对象编程却要求对象之间有方向、有目的的控制传动,从一开始,异常就是冲着改变程序控制结构,以适应面向对象程序更有效地工作这个主题,而不是仅为了进行错误处理。
1)异常的处理机制
传统的错误处理机制是通过返回值来处理错误,异常的处理机制:
1)C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。
2)异常是专门针对抽象编程中的一系列错误处理的,C++中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试。异常的超脱式函数机制决定了其对函数跨域式会跳,跨越函数。
2)异常处理语法
1)若有异常则通过throw操作创建一个异常对象并抛掷。
2)将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
3)如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
4)catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
5)如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
6)处理不了的异常,可以在catch的最后一个分支,使用throw语法向上抛。
3)栈解旋
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)。
为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型,如void func() throw (A, B, C , D); 这个函数能够且只能抛出类型A B C D及其子类型的异常。如果在函数声明中没有包含异常接口声明,则函数可以抛掷任何类型的异常,如void func(); 一个不抛掷任何类型异常的函数可以声明为:void func() throw();如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexpected函数会被调用,该函数默认行为调用terminate函数中止程序。
4)异常类:
throw的异常是有类型的,可以是数字、字符串、类对象,catch严格按照类型进行匹配。C++标准异常库继承关系:
每个类所在的头文件在图下方标识出来:标准异常类的成员:在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载;logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述;所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。
9、输入输出流
程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。C++输入输出包含以下三个方面的内容:对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。
对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。
在C语言中,用prinf和scanf进行输入输出,往往不能保证所输入输出的数据是可靠的安全的。在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(type safe)的。
C++的I/O操作是可扩展的,不仅可以用来输入输出标准类型的数据,也可以用于用户自定义类型的数据。C++通过I/O类库来实现丰富的I/O功能。这样使C++的输人输出明显地优于C语言中的prinV和scanf,但是也为之付出了代价,C++的I/O系统变得比较复杂,要掌握许多细节。C++编译系统提供了用于输入输出的iostream类库。
iostream这个单词是由3个部分组成的,即i-o-stream意为输入输出流。在iostream类库中包含许多用于输入输出的类。ios是抽象基类,由它派生出istream类和ostream类,两个类名中第1个字母i和o分别代表输入(input)和输出(output)。istream类支持输入操作,ostream类支持输出操作,iostream类支持输入输出操作。iostream类是从istream类和ostream类通过多重继承而派生的类。其继承层次见上图表示。
C++对文件的输入输出需要用ifstrcam和ofstream类,两个类名中第1个字母i和o分别代表输入和输出,第2个字母f代表文件(file)。ifstream支持对文件的输入操作,ofstream支持对文件的输出操作。类ifstream继承了类istream,类ofstream继承了类ostream,类fstream继承了类iostream。iostream类库的接口分别由不同的头文件来实现。常用的有iostream 包含了对输入输出流进行操作所需的基本信息。 fstream 用于用户管理的文件的I/O操作。 strstream 用于字符串流I/O。stdiostream 用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。 iomanip 在使用格式化I/O时应包含此头文件。
1)标准I/O流
标准I/O对象:cin,cout,cerr,clog; cout流对象cout是console output的缩写,意为在控制台(终端显示器)的输出。
强调几点:: 1) cout不是C++预定义的关键字,它是ostream流类的对象,在iostream中定义。 顾名思义,流是流动的数据,cout流是流向显示器的数据。cout流中的数据是用流插入运算符“<<”顺序加入的。如果有cout<<"I "<<"study C++ "<<"very hard. << “wang bao ming ";按顺序将字符串"I ", "study C++ ", "very hard."插人到cout流中,cout就将它们送到显示器,在显示器上输出字符串"I study C++ very hard."。cout流是容纳数据的载体,它并不是一个运算符。人们关心的是cout流中的内容,也就是向显示器输出什么。
2) 用“cout<<”输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数据的类型,并根据其类型选择调用与之匹配的运算符重 载函数。这个过程都是自动的,用户不必干预。如果在C语言中用prinf函数输出不同类型的数据,必须分别指定相应的输出格式符,十分麻烦,而且容易出 错。C++的I/O机制对用户来说,显然是方便而安全的。
3) cout流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插 人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符,并刷新流(清空缓冲区)。注意如果插人一个换行符”\n“(如cout<<a<<"\n"),则只输出和换行,而不刷新cout 流(但并不是所有编译系统都体现出这一区别)。
4) 在iostream中只对"<<"和">>"运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出 进行重载。如果用户声明了新的类型,并希望用”<<"和">>"运算符对其进行输入输出,按照重运算符重载来做。
2)cerr流对象
cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的作用是向标准错误设备(standard error device) 输出有关出错信息。cerr与标准输出流cout的作用和用法差不多。但有一点不同:cout流通常是传送到显示器输出,但也可以被重定向 输出到磁盘文件,而cerr流中的信息只能在显示器输出。当调试程序时,往往不希望程序运行时的出错信息被送到其他文件,而要求在显示器上及时输出,这时应该用cerr。cerr流中的信息是用户根据需要指定的。
clog流对象:clog流对象也是标准错误流,它是console log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。
10、STL(标准模板库)理论基础
STL(Standard Template Library,标准模板库),STL的从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。几乎所有的代码都采用了模板类和模板函数的方式。
容器:STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型,可以将我们许多重复而乏味的工作简化。容器部分主要由头文 件<vector>,<list>,<deque>,<set>,<map>,<stack> 和<queue>组成。
迭代器:迭代器在STL中用来将算法和容器联系起来,起着一种黏和剂的作用。几乎STL提供的所有算法都是通过迭代器存取元素序列进行工作的,每一个容器都定义了其本身所专有的迭代器,用以存取容器中的元素。迭代器部分主要由头文件<utility>,<iterator>和<memory>组成。
算法:C++通过模板的机制允许推迟对某些类型的选择,直到真正想使用模板或者说对模板进行特化的时候,STL就利用了这一点提供了相当多的有用算法。它是在一个有效的框架中完成这些算法的可以将所有的类型划分为少数的几类,然后就可以在模版的参数中使用一种类型替换掉同一种 类中的其他类型。STL提供了大约100个实现算法的模版函数,算法部分主要由头文件<algorithm>,<numeric>和<functional>组 成
使用STL的好处:1)STL是C++的一部分被内建在你的编译器之内,STL的一个重要特点是数据结构和算法的分离,使得STL变得非常通用。
2)高可重用性:STL中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。关于模板的知识。
3)高性能:如map可以高效地从十万条记录里面查找出指定的记录,因为map是采用红黑树的变体实现的。(红黑树是平横二叉树的一种)
4)高移植性:如在项目A上用STL编写的模块,可以直接移植到项目B上。
不用思考STL具体的实现过程,只要能够熟练使用STL就OK了。就可以把精力放在程序开发的别的方面。
1)容器
用来管理一组元素,分为顺序容器和关联容器;顺序容器->每个元素都有固定位置,取决于插入时机和地点,和元素值无关。eg (vector、deque、list);关联式容器:元素位置取决于特定的排序准则,和插入顺序无关,eg(set、multiset、map、multimap)
数据结构 | 描述 | 头文件 |
向量(vector) | 连续存储的元素 | <vector> |
列表(list) | 由节点组成的双向链表,每个结点包含着一个元素 | <list> |
双队列(deque) | 连续存储的指向不同元素的指针所组成的数组 | <deque> |
集合(set) | 由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序 | <set> |
多重集合(multiset) | 允许存在两个次序相等的元素的集合 | <set> |
栈(stack) | 后进先出的值的排列 | <stack> |
队列(queue) | 先进先出的值的排列 | <queue> |
优先队列(priority_queue) | 元素的次序是由作用于所存储的值对上的某种谓词决定的的一种队列 | <queue> |
映射(map) | 由{键,值}对组成的集合,以某种作用于键对上的谓词排列 | <map> |
多重映射(multimap) | 允许键对有相等的次序的映射 | <map> |
2)STL算法:
操作对象: 直接改变容器的内容,将原容器的内容复制一份,修改其副本,然后传回该副本。
非可变序列算法 | 指不直接修改其所操作的容器内容的算法 | |||
比较算法 equal、mismatch、lexicographical_compare | 计数算法 count、count_if | 搜索算法 search、find、find_if、find_first_of、… | ||
可变序列算法 | 指可以修改它们所操作的容器内容的算法 | |||
删除算法 remove、remove_if、remove_copy、… | 修改算法 for_each、transform | 排序算法 sort、stable_sort、partial_sort、 | ||
排序算法 | 包括对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作 | |||
数值算法 | 对容器内容进行数值计算 |