如果你觉得C++还不够复杂,那你知道protected abstract virtual base pur virtual private destructor inheritance是什么意思吗?你上次用到它又是什么时候呢?
-----Tom Cargill,C++ Journal 1990年秋
C++在软件的复用性方面或许可以比以前的语言取得更大的成功。因为C++的继承的风格基于对象,即允许数据的继承,也允许代码的继承。
1985年以前,C++的名字是“C with classes”,但是现在人们已经在其中加入了非常非常多的特性,从当时的角度类看,“C with classes”是C语言的一个相当合理的扩展,很容易理解和解释。随即,人们对这种语言投入了极大的热情,至今未曾衰减。现在的C++是一个相当庞大的语言。具体地说,一个C编译器的前端大约有40000行代码左右,而一个C++编译器的前端的代码可能是它的两倍,甚至更多。
这一节主要是对C++语言特性和一些概念的概括总结,本来不打算写了,因为具体的细节问题我在[C++ PP(Edit 6)]中也有具体的解释,可能大部分有重复,但是为了小结和复习,回顾一下C++语言与C语言的区别和一些新的特性及其相关概念,决定还是写下来,高手略过。
面向对象编程的特点是封装、继承和多态(动态绑定)。C++通过类的派生支持继承,通过虚拟函数支持动态绑定。虚拟函数提供了一种封装类体系实现细节的方法。
一、全面认识C++概念及注意的知识点:
(1)抽象(Abstruct):
它是一个去除对象中不重要的细节的过程,只有那些描述了那些对象本质特征的关键点才被保留。抽象是一种设计活动,其他的概念都是提供抽象的OOP(Object-oriented paradigm)特性。
抽象的概念就是观察一群事物,并认识到他们具有共同的主题。你可以忽略不重要的区别,只记录能表现事物特性的关键数据。当你这样做的时候,就是在进行抽象,所存储的数据类型就是“抽象数据类型”。抽象听上去像是一个艰深的数学概念,但不要被它糊弄--它只不过是对事物的简化而已。
在软件中,抽象是十分有用的,它允许程序员实现下列目标:
(1)隐藏不相关的细节,把注意力集中在本质特征上。
(2)向外部世界提供一个“黑盒子接口”,接口确定了施加在对象之上的有效操作集合,但它并不提示对象在内部是怎样实现他们的。
(3)把一个复杂的系统分解成几个相互独立的组成部分。这可以做到分工明确,避免组件之间不符合规则的相互作用。
(4)重用和代码共享。
抽象建立了一种抽象数据类型,C++使用l类(class)这个特性来实现它。
(2)类:
就是用户定义类型加上对该类型进行操作。
类是一种用户定义的类型,就好像是int这样的内置类型一样。内置类型已经有了一套完善的针对它的操作(如算术运算)等,类机制也必须允许程序员规定他所定义的类能够进行的操作。类里面的任何东西被称为类的成员。
一个空类有:构造函数,析构函数,拷贝构造函数,赋值操作符operator=四个成员函数。下面会分别介绍。
C++的类机制实现了OOP的封装要求,类就是封装的软件实现。类也是一种类型,就像是char,int,double和struct st*一样。因此在使用之前必须要声明类的变量
,类和类型一样,你可以对它进行很多操作,如取得它的大小或声明它的变量等。对象和变量一样,可以对它进行很多操作,如取得它的地址、把它作为参数传递、把它作为函数的返回值、使它成为常量值等。
(3)对象(Object):
某个类的一个特定变量,就像j可能是int类型的一个变量一样。对象也可以被称作类的实例(instance)。
(4)封装(encapsulation):
把类型、数据和函数组合在一起,组成一个类。在C语言中,头文件就是一个非常脆弱的封装的实例。它之所以是一个微不足道的封装例子,是因为它的组合形式是纯词法意义上的,编译器并不知道头文件是一个语义单位。
(5)单继承(ingheritance):这是一个很大的概念--允许类从一个更简单的基类中接收数据结构和函数。
派生类获得基类的数据和操作,并可以根据需要对它们进行改写,也可以在派生类中增加新的数据和函数成员。在C语言里不存在继承的概念,没有任何东西可以模拟这个特性。当一个类沿用或定制它的唯一基类的数据结构和成员函数时,它就成了单继承。不要把在一个类内部嵌套另一个类与继承混淆。嵌套只是把一个类嵌入另一个类的内部,
称内部类。
(6)多继承:多重继承允许把两个类组合成一个类,这样的 结果类对象的行为类似于这两个类的对象中的一个。
(7)虚继承:主要解决在多继承中,访问不明确的问题。虚继承中所有的子类都指向同一份数据空间。
(8)虚基类:虚继承的父类称之为虚基类,它表示积累是被多个多重继承的类所共享。(编程的时候一般避免虚继承)
(9)内部类(Inner):内部类与外部类的关系:与编译器有关,
VC6.0他们是两个完全不同的类,不能用对方的东西,如果要用,可以定义为友元类;VS2005编译器,内部类可以用外部类的东西,外部类不能用内部类的东西。
内部类的构造函数运行的顺序:先调用外部类的构造函数,如果有父类则先调用父类的构造函数。然后再调用自己的构造函数。
(10)纯虚函数(pure virtual function):纯虚函数是一种特殊的虚函数,在许多情况下,基类不能对虚函数给出有意义的实现,而把它申明为纯虚函数,它的实现留给基类派生的类去做。这就是纯虚函数的作用(类似于java中的接口)。同时含有纯虚函数的类称为
抽象类(至少含有一个纯虚函数),它不能生成对象。
class father
{
public:
virtual void show()=0;//纯虚函数;
}
(11)模板(template):这个特性支持参数化类型。
是一种泛型技术,用不变的代码实现可变的算法。
#include <iostream>
using namespace std;
template<typename T>//函数模板不能给默认值;
T Min(T a,T b)
{
return a>b?b:a;
}
template<typename T,typename R>
void show(T a,R b)
{
cout<<"a:"<<a<<" b:"<<b<<endl;
}
void show1(T c)//一个函数模板也不能被多个函数来使用;
{
cout<<c<<endl; //error,不认识;
}
template<typename T,typename R=int>// 只有类模板才可以给默认值;
class father
{
public:
T a;
R b;
};
template<typename T,typename S>
class Son:public father<S>//在父类的后面加上<S>表示子类用S来给父类进行初始化;
{
//初始化;
};
int main()
{
cout<<Min(34,2)<<endl;
show(23,"kongyin");
system("pause");
return 0;
}
(12)内联函数(inline):程序员可以规定某个特定的函数在行内以指令流的形式展开(就像宏一样),而不是产生一个函数调用,避免了函数调用的过程,提高了程序的执行效率。
(13)异常处理(exception):异常通过发生错误时把处理自动切换到程序中用于处理错误代码的那部分代码。C标准库提供两个特殊的函数:setjmp() 及 longjmp(),这两个函数是结构化异常的基础,正是利用这两个函数的特性来实现异常。setjmp和longjmp的主要作用是错误恢复,
前面有详细的讲过。只要还没有从函数返回,一旦发现一个不可恢复的错误,可以把控制转移到主输入循环,并从那里重新开始运行。
try{} cathch{}异常处理是C++特有的。实例如下:
#include <iostream>
using namespace std;
void show(int num)
{
switch(num)
{
case 1:
cout<<"throw:";
throw num;
case 2:
cout<<"throw:";
throw "ERROR";
}
}
int main()
{
try
{
show(1);
}
catch (int a)//抛出是什么类型的就要用什么类型的来接收。
{
cout<<"catch:"<<a<<endl;
}
catch (char *str)
{
cout<<"catch:"<<str<<endl;
}
system("pause");
return 0;
}
(14)友元函数(friend):
属于friend的函数不属于类的成员函数,但可以像成员函数一样访问类的private和protected成员。friend可以是一个函数,也可以是一个类。
(15)成员函数(member function):调用一个类的对象的成员函数相当于面向对象编程语言所使用的“向对象发送一条信息”这个术语。
每个成员都有一个this指针参数,它是隐式的赋给该函数的,它允许对象在成员函数内部引用对象本身。注意,在成员函数内部,你应该发现this指针并未显示出现,这也是语言本身所涉及的。
#include <iostream>
using namespace std;
class father
{
private:
int number;
public:
void show()
{
cout<<"this的地址:"<<this<<endl;
}
};
int main()
{
father ff;
cout<<"对象的地址:"<<&ff<<endl;
ff.show();
system("pause");
return 0;
}
运行结果:
(16)构造函数(Constructor):绝大多数类至少有一个构造函数。当类的一个对象被创建时,构造函数被隐式的调用,它负责对象的初始化。构造函数是必要的,因为类通常包含一些结构,二结构又包含很多字段。这就需要复杂的初始化。当类的一个对象被创建时,构造函数会被自动调用。
(17)拷贝构造函数(Copy Constructor):拷贝构造函数也是一种特殊的构造函数,函数的名称和类名一致,它的唯一一个参数是本类型的一个引用。a,当用一个已初始化的自定义类型的对象去初始化另一个新构造的对象的时候;b,当函数的参数是类的对象的时候;c,函数的返回值是类的对象的时候,拷贝构造函数就被调用。
(18)深拷贝和浅拷贝(位拷贝):简单理解:深拷贝,发生对象的复制,资源的复制;浅拷贝只是指针的复制,没有资源的复制。
(19)析构函数(DesConstructor):与构造函数相对应的,类也存在一个清理函数,称为析构函数。
当对象被销毁(超出其生命周期或进行delete操作,回收它所使用的堆内存)时,析构函数被自动调用。有人把析构函数当作一种保险方法来确保当对象离开适当的范围时,同步锁总能够释放。所以它们不仅能清楚对象,还清理 对象所持有的锁。构造函数和析构函数都是必要的,因为类外部的任何函数都不能访问类的私有成员。因此,你需要类内部有一个特权函数来创建一个对象并对其进行初始化。
但是构造函数和析构函数都违背了C语言中“一切工作自己负责”的原则。他们可以使大量的工作在程序运行时被隐式的调用完成一些功能,减轻程序员的负担,这也违背了C语言的哲学,也就是语言的任何部分都不应该通过隐藏的运行时程序来实现。
(20)重载(overload):就是简单的复用一个现存的名字,但使它操作一个不同的类型。可以是函数的名字,也可以是一个操作符。
该函数的返回值、参数的个数、类型和顺序都可以不同。重载不能是私有的,不能降低访问权限,否则不是重载。重载都是在编译器解析。
(21)重写(overwrite):函数的名称、参数和返回值一样。
仅限于父子之间的返回类型可以不同。多态是在重写的基础上实现的。
(22)多态(polymorphism):源于希腊语,意思是“多种形状”。
根据不同类型调用不同的函数的能力,允许父类的指针可以指向子类的成员函数,允许对象与适当的成员函数在运行时进行绑定,运行时根据对象的类型选择正确的成员函数。也称迟后编译和滞后编译(late binding)。基类有多少子类,父指针就有多少形态。主要通过虚函数来实现,关键字为virtual。它用来通知编译器,该派生类的成员函数有可能取代基类的同名函数。
原理是:单继承通常通过在每个对象内包含一个vptr指针来实现虚拟函数。vptr指针指向一个叫做vtbl的函数指针向量(称为虚拟函数表,也称V表)。每个类都有这样一个向量,类中的每个虚拟函数在该向量中都有一条记录。使用这种方法,该类的所有对象共享实现代码。通过此虚表来实现。是一种泛型技术。虚表的首地址是对象的首地址。
(23)虚函数(virtual):
不能被声明为虚函数的是:普通函数,类的静态成员函数,inline函数,friend函数(不是类的成员函数,C++不支持)和构造函数。能声明为虚函数的条件是:(1)能被继承;(2)类的成员函数
(24)静态成员函数:
静态成员函数是类的组成部分,但是不是任何对象的组成部分,所有对象共享一份,没有必要动态绑定,也不能被继承【效果能,但机理不能。静态成员函数就一份实体,在父类里;子类继承父类时也无需拷贝父类的静态函数,但子类可以使用父类的静态成员函数】,
并且静态成员函数只能访问静态变量。
(25)new和delete操作符:用于取代free和malloc函数。这两个操作符用起来更方便一点。
如能够自动完成sizeof的计算工作,并能调用合适的构造函数和析构函数。new能真正的建立一个对象,则malloc只是简单的分配内存。
(26)传引用调用(call-by-reference):相当于传址调用,C语言只使用传值调用(call-by-value)。C++在语言中引入了传引用调用,可以把对象的引用作为参数传递。
二、C++对C语言的改进
(1)类型转换既可以写成像float(i)这样看上去更顺眼的形式,也可以写成像(float)i这样稍显怪异的C语言风格的形式。
(2)C++允许一个常量整数来定义数组的大小;
const int size =128;
char str[size];
在C++中这是允许的,但是在C中是不允许的。
(3)声明可以穿插在语句之间。在C语言中,一个语句块中所有的声明都必须放在所有的语句的前面。C++去掉了这个专横的限制,声明可以出现在语句可以出现的任何地方。
虽然C++显的复杂,但是它是对C语言唯一成功的改造方案,拥有大群的支持者。
三、C++小知识点汇集(参考 钱能 著《C++程序设计教程(修订版)》)<知识点可能有重复>
1.class里默认的情况下的成员是private;struct默认是public;结构体中不能有成员函数;
2.::叫作用域区分符。党成员函数在类外实现时,前面必须附加此前缀“::”,它仿佛在大声呐喊,“嗨,我很重要,我表示有一些东西属于这个类”,
指明一个函数和成员属于哪个类。::可以不跟类名,表示全局函数,即非成员函数;
3.类中定义的成员函数都是内联inline函数,这样在调用的时候就不会拷贝一份,而是指针直接指向函数的入口地址,直接调用,提高了调用的效率;
4.成员函数一般规模都比较小,所以switch语句不允许用;
5.函数声明一般在头文件,而函数的定义不能再头文件,因为它们将被编译多次;使用#pragma once 表示只调用一次;
6.inline函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数。与非inline函数不同的是,inline函数不是一次函数的跳转,
而是指令的展开(从而提高执行效率)。如果内联函数过大,就会导致目标码过大,增加额外的换页行为,降低指令高速缓存装置的击中率。
所以编译器会自动变为非内联;
7.由于类中的成员函数都是内联inline的,所以避免了不能被包含早头文件的问题;
8.因为类名是成员函数的一部分,所以一个类的成员函数与另一个成员函数重名也不能认为是重载;
9.类的对象所占的空间由他的数据成员所占据的空间综合决定,类的成员函数不占据对象的空间;
10.类的对象有全局对象,局部对象,静态对象和堆对象--指针对象;
11.构造函数是类的成员函数,可以有任意多个参数,可以重载;所以可以放在类外定义;没有返回类型,当然也没有返回值;但可以有无值返回语句return;
没有构造函数就不能创建任何对象;
12.析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能重载,对象生命周期结束的时候,系统自动调用;
13.类中对象和函数的定义与声明:
class Tdate{}
Tdate day();这是声明了一个day的普通函数,返回的Tday类对象;
Tdate day(int);创建了一个函数;
Tdate day(10);创建对象;
14.类的析构函数将自动地为各个具有析构函数的数据成员调用析构函数;
15.类的定义是不分配空间和初始化的;
16.类是一个抽象的概念,并不是一个实体,并不含有属性值,而只有对象才占有一定的空间,含有明确的属性值。
17.在构造函数的后面,冒号表示要对类的数据成员的构造函数进行调用;冒号语法是的常量数据成员和应用数据成员的初始化成为可能;因为常量是不能被赋值的,引用变量也是不可重新指派的,初始化之后,其值就固定不变了。
class student{
public:
student(int &i):ten(10),refi(i){}
protected:
const int ten;//常量数据成员;
int &refi; //引用数据成员;
student()
{
ten=10; //error:常量不能赋值;
refi=i; //error:引用不能重新指派;
}
}
18.对于类的的数据成员是一般变量的情况,则放在冒号后面与放在函数体重初始化都一样;
19.创建对象的唯一途径是调用构造函数;
20.局部和静态对象是指块作用域和文件作用域的对象;
静态对象只被构造一次:文件作用域的静态对象在主函数开始运行前全部构造完毕。块作用域的静态对象,则在首次进入到定义该静态对象的函数时,进行构造;
class student{
public:
int n;
student( int m_n)
{
cout<<"construct student"<<m_n<<endl;
}
}
void fn(int m)
{
static student st(m); //局部静态构造函数;
cout<<"fn: "<<m<<endl;
}
int main()
{
fn(10);
fn(20);
}
运行结果:
construct student 10; //构造函数只执行一次;
fn: 10;
fn: 20;
21:成员变量和成员对象的初始化和构造顺序以其在类中声明的顺序构造;不是按照构造函数冒号后面的顺序执行;
22.面向对象程序设计基于两个原则:抽象和分类;---结构化程序设计;效率的比较:
一个人体力很好:他可以骑车穿越大街小巷到达目的地,效率是比较高,但是要是两地相隔甚远,那么就不那么容易了,,就要选择火车,飞机,只要付钱坐上去就可以了,不用考虑中间的过程,这就是面相对象;
23.程序运行的效率是指占尽可能少的资源【存储空间】而运行的速度相对较快,不能片面的看某一方面,要综合比较时间和空间客观的评价一个程序运行的效率;
24.全局数据区:全局变量,静态数据,常量;
代码区:类的成员函数和非成员代码;
栈区:为运行函数儿分配的局部变量,函数参数,返回数据,返回地址;
堆区:余下的空间放在堆区;
25 类中需要new和delete的原因:
因为malloc在分配空间的时候不能调用构造函数,类对象的建立是分配空间,构造结构以及初始化的三位一体,他们统一是由构造函数完成;而malloc只是获得一个含有随机数据的类对象空间而已;对应的对象空间中的值不确定,为此,必须在内存分配之后再进行初始化;这从根本上来说,不是类对象的创建,它绕过了构造函数;
指针对象只有在调用delete时,才调用析构函数;如果是局部对象,则在该局部对象退出作用域时【结束语句},或者return时】,自动调用析构函数;
26.在分配对象数组时,类型的后面只能跟[元素的个数],不能再跟构造函数的参数,所以,从堆上分配对象数组,只能调用默认构造函数,不能调用其他;如果该类没有默认的构造函数则不能分配兑现该数组;
student st[4];
student *st[4];
delete
[] st; //其中的[]是告诉c++这是一个数组,填上数字,编译器会忽略;但是如果不写,就会产生错误;
27.一个空类的默认的函数:构造函数,拷贝构造函数,析构函数,运算符重载函数,操作符重载函数operator=;
28.实例对象,指针对象【堆对象】,临时对象,无名对象
class student
{
int numnber;
}
studnet fn()
{
studnet ms(200);
return ms;
}
student st(100);//实例对象
studnet *st =new studnet(100);//指针对象
student(100);//无名对象
studnet &sts=fn();//非常危险,不再有效;
29,无名对象的典型三种用法:注意初始化引用和初始化对象的区别,还有赋值和初始化的区别;
void fn(studnet &s);
int main()
{
student &refs=studnet("Jenny"); //初始化引用; 在函数内部,无名对象作为局部对象产生在栈空间中; studnet refs=Jenny;
student s=student("Jenny");//初始化对象定义; 用无名对象去拷贝构造一个对象s;studnet s=Jenny;省略创建无名对象这一步;按理说:C++先调用构造函数 “studnet (char *);”创建一个无名对象,然后再调用一个拷贝构造函数 studnet (student &)来创建对象S;但是由于是用无名对象去拷贝构造,完事儿后该无名对象就失去了意义,所以省略了创建无名对象这一步;
fn(studnet("Jenny")); //函数参数; 无名对象作为实参传递给形参s,先调用构造函数创建一个无名对象,然后将该无名对象初始化给引用参数s对象,有于实参是在主函数中,所以无名对象是在主函数的栈区中创建,函数fn()的形参s引用的是主函数栈中的一个对象;
student s("Jenny");
fn(s);
}
30.由于C++默认的构造函数只是对对象进行了浅拷贝复制,如果对象的数据成员包括指向对空间的指针(堆空间),就不能用这种方式,必须自己定义拷贝构造为其分配空间,也就是深拷贝,资源的拷贝;