Language Cpp C

    2019年03月22日 字数:4152

浅谈C与C++

2018141461112 计算机拔尖班夏文羿

摘要:

自19世纪70年代初期在贝尔实验室诞生了Unix操作系统与C语言,C语言随时间有很多的发展,包括ANSI C语言与K&R C语言,两者标准不同。其中划时代的发展便是引入了类,这便是早期的C++,称为C with classes,而C++11标准的出台则是现代C++出现的标志,使C与C++之间有了更多差异,虽然C++与C同出一源,但着实不可把他们在看作同一语言。C++作为面向对象语言的翘楚被大多数软件工程师喜爱,而C语言其与硬件的相关性让很多机械工程师青睐。

关键字:

类,抽象,指针,异常,泛型编程。

C语言与C++的发展历史:

C语言的发展史可以大致分为几个阶段,从1965年BCLP到1969年的B语言再到1971出现了New B语言,到1972年才出现了早期的C,1978年出版的C语言经典名著The C programming Language 让其作者Brian Kernighan 和 Dennis Ritchie 因此名声大噪,而这个版本的C语言也因此被称为K&R C,C语言成为那20年最成功的高级语言之一,而现在仍然如此。1983年美国国家标准化组织ANSI成立了C语言工作小组,开始了C语言的标准化工作,这也就诞生了作为标准的ANSI C,而1990年初,ANSI接受了国际标准化组织的ISO C,1989年第一个标准C诞生也就是C89,而1999年这个被更新成C99,直到2011年出现的C11标准,至此。

而C++的出现则是对C语言的一个继承,20世纪70年代中期,20世纪70年代中期,Bjarne Stroustrup在剑桥大学计算机中心工作。他使用过Simula和ALGOL,接触过C。他对Simula的类体系感受颇深,对ALGOL的结构也很有研究,深知运行效率的意义。既要编程简单、正确可靠,又要运行高效、可移植,是Bjarne Stroustrup的初衷。以C为背景,以Simula思想为基础,正好符合他的设想。1979年,Bjame Sgoustrup到了Bell实验室,开始从事将C改良为带类的C(C with classes)的工作。1983年该语言被正式命名为C++。自从C++被发明以来,它经历了3次主要的修订,每一次修订都为C++增加了新的特征并作了一些修改。第一次修订是在1985年,第二次修订是在1990年,而第三次修订发生在c++的标准化过程中。在20世纪90年代早期,人们开始为C++建立一个标准,并成立了一个ANSI和ISO(Intemational Standards Organization)国际标准化组织的联合标准化委员会。也就是所谓的C++标准委员会。C++98成为第一个标准的C++语言,而C++不断的加入新特性,如C++11的出台也标志着C++语言进入了Modern C++。之后的C++14,C++17,甚至还是概念的C++20标准,都是增添了很多新特性。

C++与C语言的差异

结构体struct的差异,C++引入了class之后任然保留的C的struct,但对于C语言的struct C++的struct 可以说功能更强大。在C语言中,struct 仅仅只是一堆变量的合集,不具备成员函数,也没有构造函数,也仅仅是将一些有关联的变量放在一起罢了。 代码例子

struct Person{
	int high, weight;
	char ID[18];
};

但在C++中就不同,C++引入了成员函数,也有了构造函数,这已经是面向对象的基础了。

struct Person{
	int high, weight;
	string ID;
	Person(){
		high = weight  = 0;
		ID.clear();
	}
	Person(int _high, int _weight,string _ID):high(_high),weight(_weight),ID(_ID){}
};

那么有个问题,C++的struct 与C++ 的class又有什么不同呢,我们知道,class有三种访问的保护模式,分别是public,protected,private,其中private的权限最高,只有类内部和友元函数可以访问。而struct 和class 的区别就在于,未声明是何种访问类型时,struct默认是public 而class 默认是private。我们不希望我们的信息可以被大家任意修改,我们只希望被他人询问的时候才告诉他我们的信息。我们最好通过指针来指向对象的地址来访问对象,这样安全的多。

class Person{
private:
	int high, weight;
	string ID;
public:
	Person() = default;
	~Person() = default;
	Person(int _high, int _weight, int _sex):high(_high),weight(_weight),sex(_sex){}
	int get_high(){
			return this->high;
	}
	int get_weight(){
		return this->weight;
	}
	string get_ID(){
		return this->ID;
	}
};

这样就可以阻止person的对象的信息被修改。

面向过程与面向对象

面向过程很好理解,我们解决问题的时候,通常需要分出我们的步骤,比如我们要吃菜那么假设有这么几个过程,买菜,洗菜,切菜,炒菜,做菜,然后吃。我们可以依次写几个函数来描述那么几个过程类似void 买菜(一颗翠绿的白菜)。然后一一调用他们就行了,这便是面向过程编程。而面向对象编程我们重点关注的将是 谁 去执行这个过程,那么这个过程会是怎么样呢,比如这时候有个菜的对象,一颗翠绿的白菜。

一颗翠绿的白菜.被买(),一颗翠绿的白菜.被洗(),一颗翠绿的白菜.被切()等等,这里我们就要抽象出菜的属性和行为之后对象的属性会怎么变化,再有一个通俗的例子,我们要打开我们教室的窗子,那么面向过程就是,打开窗子(教室的窗子),而面向过程则是,教室的窗子.打开()。

当然这只是稍微阐述了面向过程与面向对象的分别,具体实现还是一个不太容易的过程。

面向对象涉及了很多东西,比如它具有封装性,继承性,多态性。

封装性很好理解,比如一台洗衣机便是一个封装好的东西,我们需要知道怎么制造一台洗衣机才能使用他么?当然不,制造洗衣机的人帮我们造出了他,并且留下了几个按钮,也就是接口来帮助我们使用他,我们也就是用户,不需要知道他内部怎么运行的,我们只需要知道,按下这个按钮能够让他开始运转帮我们洗好衣服就行了。封装就是如此,将一些方法封装好,用户不需要知道这个方法具体是怎么实现的,只需要知道调用了这个成员的这个方法可以达到什么效果就行了。

继承性涉及到了子类与父类之间的一个抽象,我们人有那些属性,比如我们可以抽象出我们都有年龄,都有ID,都有体重,而我们再细化一下类,比如学生和老师,学生和老师都是人,他们都继承了父类人的所有内容,在这里对于学生而言,他的ID是学号,而对于老师而言他的ID 是工号,学生又有自己属性,比如年级,院系,而老师则有职称这类的属性。而在JAVA 中他只存在一种继承关系,也就是单继承is a模型,比如学生is a 人,老师 is a 人,班长is a 学生。需要用到多继承的地方将要用到一个叫interface 的东西,也就是接口。而在C++中存在多继承,比如辅导员,既是老师又是学生,他同时继承了老师和学生这两个类,这就形成了一个菱形继承关系,而再继承的过程中子类会复制父类的属性,那这样辅导员就会复制两遍他的父类的父类人的属性,这样是我们不希望看到的,不仅会产生隐患(二义性)还浪费资源,那么C++是怎么处理这个问题的呢,我所知道的是两种解决方式,一是虚基类,表明人 是个虚基类这样就可以避免二次复制人的属性,但是虚基类不能被实例化,另一种方式则是虚继承。

多态性则是在父类与子类之间相似行为的描述,多态是在运行时多态,必须要声明父类得函数为虚函数才能实现多态,举个例子,菜这个基类,有几个子类,白菜类,萝卜类,土豆类,然后洗菜这个方法,对于每个子类都有,但是实现得方法不同,那么我们就在菜这个基类中声明洗菜函数为虚函数,然后在子类中一一实现这些方法就行。虚函数使得多态得以实现,而多态则是一个运行时多态并非编译时。在JAVA中,interface就是虚函数得合集,一种接口多种实现。虚函数的实现原理就在于编译器在编译时回构建一个虚函数表。函数重载也可以称为多态,而这个多态被称为编译时多态,而虚函数则是运行时由指向具体对象的类来决定运行那个函数,所以也叫运行时多态,这两种多态各有优缺点,虚函数的实现也引入了泛型编程的概念。

面向对象编程的特点就是继承和东陶绑定。C++通过类的派生支持继承,通过虚函数支持动态绑定,虚函数提供了一种封装类体系实现细节的方法。

抽象是个很大得学问,所谓抽象就是抽取事务的本质,作为属性封装到类里。类的设计,模式得选择都会影响到之后程序得编写与日后得维护,这也就是所谓得可复用得面向对象程序设计,也叫设计模式,设计模式是C++中非常重要的一个部分,这里不再深究。

内存管理

C语言的内存分配我们用的多的是malloc函数来分配内存,malloc的缺点在于容易越界,因为他是由程序员来设定到底分配多大的内存如int* p = (int*)malloc(1), 一个int型变量最少也占4字节,而只给p指针分配了一个字节的内存,这就造成了访问越界,也就是缓冲区溢出,很有可能访问越界之后就会覆盖掉原本程序存储的合法的数据造成数据的丢失或者引发其他的运行错误。第二个缺点也很明显,就是不可以初始化。对于每一个malloc我们最好都给出一个free释放掉他的内存,防止放生内存泄漏。malloc函数是在堆中分配内存而且返回值不安全,最重要的是malloc需要自己计算开辟多大的内存这在某些时候我们并不知道具体需要多少内存时,就容易造成访问越界。而在C++中,我们则用new关键字来给一个指针分配动态内存,相应的我们需要给出delete来释放我们分配的内存。C++的new虽然可以给数组初始化,但是也只可以做0初始化。new不再需要我们自己计算需要分配多大的内存了,他从自由存储区域开辟且返回值是安全的,在分配内存出错时new会抛出异常,而malloc返回值为0。 构造函数用于一个类型实例化时的初始化,而析构函数则是在该类的对象生命周期结束后自动调用的,作用是释放该对象占用的内存。每一个对象都应当在他声明周期结束后调用特德析构函数来释放掉他所占的内存,并不是说要节约内存,更大的作用是防止内存泄漏,防止出现什么难以预料的错误。

异常

异常是指存在运行时的反常行为。在C++和JAVA中异常都被认为是非常优雅的错误处理机制。而在C语言中并没有此概念。需要注意的是,一个函数是否捕获一个异常应当取决于这个函数是否可以很好的处理这个异常,而让其他异常穿过函数,寻找到下一个有能力处理这个异常的函数来捕获他。

模板与泛型编程

模板这个东西在C++中是非常重要的东西,不会玩模板怎么能说会写C++。泛型编程也是可复用型程序设计的一部分,面向对象编程和泛型编程都可以处理在编写程序的时候不知道类型的情况,不同之处在于,面向对象编程能处理类型在程序运行前都不知道的情况,而在泛型编程中,在编译时就能获知类型了。例如我们需要比较两个相同类型且已经重载大于号运算符的变量的大小并返回较大者,我们可以这么做

template <class T>
T max (const T& a, const T& b){
	return a > b? a: b;
}

这样我们可以将两个类型未知的变量取较大值返回。 而有一种类叫模板类,例如STL中的vector<>就是一个模板类,其特点是容器,可以容纳任意相同类型的对象。模板类还有另一个作用就是类型擦除,实现any容器。就好比python中的元组可以容纳任意的东西,而python这门语言写起来让人非常的愉快就在于他的自由以及简短。这里简单说一下python与C/C++的一个小不同之处,我们注意到python是不用声明变量类型的,因为python时动态强类型变量,而C/C++是静态弱类型变量,这两者的区别在于,python虽然不需要声明变量,但在定义之后不允许隐式转换,而在C/C++中,我们会发现虽然我们声明了类型但在实际的操作中我们会发现他存在一种我们看不见的类型转换,也就是隐式转换,比如把一个int和一个long long相加,他会默认把两个都转换成long long再相加。隐式转换是危险的,我们应当尽量避免出现隐式转换。加上explicit关键字就可以阻止隐式转换的发生。 再说回我们的模板,模板实现的原理在于编译器来推断你这个地方调用的这个函数是什么类型,所以是再编译的时候就已经知道类型了,我们管这个过程叫实例化一个特定版本的函数来实现这个功能。所以简便是因为编译器帮我们做了大多数事,从而也节省了程序员的时间。 auto关键字可以用来自动推断变量的类型,这个过程也是在编译阶段完成的,与template类似。

STL模板库

STL全称是Standard Template Library,标准模板库,很多在初中或高中就接触算法竞赛的学生用C++其实就是写的是C with STL,在算法竞赛中,我们并不关注抽象这一层面,只用面向过程式编程就可以很好的解决问题。由此也可见STL 模板的功能强大,NOI系列算法竞赛中,曾经C++没有解禁STL ,但在2001CCF宣布解禁STL后,C++便成了该赛事的主流语言,而曾经的主流语言Pascal到2020年将被禁用,这也在说明一个时代的更替,STL 下的一些非常实用的库,如algorithm库,里面有std::swap, std::max, std::min,等等。还有sort排序函数。这些都用到了泛型编程,可以对任意类型的容器调用这些函数,前提是需要支持比较符号。STL的一些容器也非常实用,如string, priority_queue, 等等。

命名空间

很多时候我们会自己重载一些函数,或者代码之间用了相同变量名,那么我们怎么区分他们,到底该调用哪一个函数,而我们希望每次调用的时候都能准确调用我们需要的那一个。这个时候我们就可以用到命名空间namespace了,在算法竞赛中我们通常在头文件底下加一行using namespace std; 这行代码表明这个程序使用std这个命名空间的东西,而不写这一行就需要这样写std:: cout 但using还可以有一种用法类似于宏定义#define,但我们需要尽量避免使用宏定义。比如可以using std::cout;这样就说明我们使用的是std命名空间的cout变量。STL模板库的所有东西都是在std这个命名空间下的。std也就是标准的意思,标准命名空间。类内声明函数但在类外重载函数也会用到相似的东西,例如

class Person{
private:
	int weight;
public:
	Person() = default;
	~Person() = default;
	int get_weight();
};
int Person::get_weight(){
	return this->weight;
}

::操作符就表示域操作符,声明这个符号后的东西属于符号前的域。比如声明迭代器iterator我们要清楚我们要声明的是什么容器的迭代器,比如我们要声明一个集合std::set s的迭代器我们可以这么写 std::set::iterator it = s.begin(); 表明了一个std::set类的迭代器 it指向了s容器的第一个地址。

C/C++

至今仍然会被放在一起谈论,他们很相似,但也很不同,C++很好的保留了C语言的面向过程的机制,与自己的新特性面向对象所共存着,根据程序员们的需求而选择使用。

参考文献:

-Peter Van Der Linden. Expert C Programming. -Andrew Koenig, Barbara Moo. Ruminations on C++. -Andrew Koenig.C trap and Pitfalls. -Stanley B. Lippman, Josee Lajoie, Barbara E. Moo .C++ Primer

请我喝杯咖啡

取消

感谢您的支持,我会继续写出更优秀的文章!

扫码支持
扫码支持
请我喝杯咖啡

打开微信扫一扫,即可进行扫码打赏哦

comments powered by Disqus