c++与java总结
编程的本质:抽象,即将现实世界的待解决问题经过抽象分析后描述给计算机,并让计算机处理。
这种描述给计算机的工具叫计算机语言。计算机只懂二进制,但计算机语言有很多种,要么直接描述、要么通过一次或多次中间过程的翻译再描述给计算机。如最早的 机器指令,机器语言经过符号表示简化后的汇编语言,简单易懂的c语言,有多种开发方式的c++,面向对象的java、c#等。
===============================================================================
编程的内容:数据结构+算法。
这里的数据结构指广义上的在计算机中实现的数据类型,如c语言中的基本类型int、double、char...,组合类型struct、数组...,甚至包括面向对象语言中的类。这里的算法也是指宏观上的程序结构组织方式与构建过程步骤。这样我们在算法的指导下使用这些基本的数据类型或者通过自定义数据类型(类)来一步一步搭建出程序,这就叫“编程”。这个‘搭建方式’根据不同的语言可能有不同的指导思想、开发方式。比如c语言通过有限的基本数据类型和组合类型来搭建程序,而且程序功能也是通过一个个函数的参数传递、调用过程来实现,这称为“面向过程的程序设计”。又如java语言通过封装得到的有层次的类组织程序,由类实例化的对象之间的请求-响应完成程序功能。这称为“面向对象的程序设计”。
总之,要解决问题,首先考虑抽象出什么样合适的数据类型来描述,然后着手算法,最后具体编码实现。例如面向对象时,对于问题抽象出什么样的类(接口与抽象类)?用UML做出哪些图来描述这个‘算法’?最后噼里啪啦的落代码。
===============================================================================
对于面向过程和面向对象的具体理解。
面向过程:设计方法上,采用从上到下,逐步细分的方法展开;结构化、分模块是其质量保证的指导;程序组织上,是以函数的相互调用为主,从main函数开始的函数调用、数据流动的过程构成一定功能的程序。
面向对象:设计方法上,封装、继承、多态组成以类的层次结构;程序以类实例化出的对象之间的请求-响应完成程序功能。
(这就是为什么面向过程分析用数据流图等,面向对象用类图、序列图等的理由)
其中在数据类型上,面向过程是用有限的数据类型构建程序;面向对象是通过构建新的数据类型来适应问题的变化。也就是说面向过程在处理问题时的解空间是小于问题域的,当问题规模不断扩大,需要的模块呈指数级递增,模块间数据传递五花八门时,程序就变得难以开发和维护了(难怪软件界曾有软件危机一说)。而面向对象寻求的是在解空间的对象和问题域的元素之间建立一一映射,通过类的层次结构 化解规模越来越大的问题。
这两种在数据类型上的处理也不同。定义一种数据类型就包括了该数据类型的值的集合,以及其上的操作的集合。如c语言这种面向过程的语言,它的数据类型的操作集合仅仅是简单、通用的加减乘除等。就算用结构体定义一个组合类型,关于这个组合类型的操作集合(各种函数)也是松散的,需要程序员在实际开发中自行处理函数调用与参数的传递。这就是所说的“从高层到底层,从抽象到具体‘通吃’”,这种通吃让c程序员开发负担重,也很难接手别人的程序,自然维护、扩展就困难了。但是面向对象就不同了,对象的属性和对属性操作(方法)是一个整体,当程序规模不断扩大,通过类的层次结构组织出更大的类依然是一个整体。
还有这两种在运行方式上也大不相同,面向过程在计算机中是一个一个函数的调用过程伴随数据流动完成程序功能。而面向对象是一个一个占独立内存空间、有生命周期的对象,他们之间的通信(请求-响应模式)完成程序功能。面向对象的这种设计是很牛的,因为它很好的适用于分布式程序设计,还有远程调用等,这样对象就存在不同的主机中,而请求-响应的这种服务通信方式不就是恰到好处?《Thinking in java》的作者在书中说应该把对象看成“生命体”,而构建面向对象程序就是构建一个可“成长”“进化”的更大生命体。
对于具体计算机语言,说说c语言吧,貌似上面陈述面向过程和面向对象时,全是在贬低c语言这种面向过程的程序设计方式。其实不然,至少我没看轻、贬低任何语言的资格。用咋大中国的话说:存在就是道理!c语言在那个时代也是功不可没的,只是这个时代的计算机要处理的问题规模越来越大,c语言有点水土不服吧。我承认面向对象思想是比c语言这种面向过程的,无论是指导思想,还是开发方式、运行方式上都要高级。然而面向对象语言在加上层层的运作机制和对象的“外衣”后,其运行效率确实差了,而且对硬件的处理上在底层上也多依赖c语言等面向过程的语言,比如java高大上的list集合的底层实现是调用的一个native静态方法arraycopy(,,,,),这这个本地方法是由c语言实现的。也就是说面向过程的c语言仍站一席之地。
===============================================================================
下面通过与java的对比,总结下这几周的c++学习:
【1】、数据类型
java:大体上分基本类型(可包装成引用类型)、引用类型。
c++ :大体上分三种:
基本类型:整型、浮点型、字符型。
组合类型:数组、指针、结构体、共用体、枚举等。
类类型 :string和自定义类等。
(1)、在表达式赋值和函数传值、返回值时:
java基本类型是传值,引用类型没有选择余地都是传引用。
c++可以选择指针和引用类型,也可以选择传值(复制)。
而且c++的函数传值、返回值也没java那么单纯,很多涉及安全、效率的问题。(我另一篇总结)
(2)、c++的‘引用’与java的‘引用’完全不同,是完完全全不同。
如Java中:Student s1;
c++中 :Student s2;
s1这句在java是声明的一个Student类对象的引用,只是一个引用,引发Student类的加载和链接但并没有初始化,这个引用保存在栈。当s1= new Student("abc");这条语句后,在堆中创建了一个Student类的对象,而这个s1指向这个堆中的对象。s1=new Student("bcd");接下来这个s1还可以指向任何String类的对象。
s2这句在c++中是直接创建一个默认值的Student类的对象s2。也就是说c++中 ,Student s2与int a是一个意思,Student s2("abc")与int a=2也是一个意思,即默认在栈上分配空间并新建一个变量,不同的是对于自定义类型可以自由确定对象及成员的位置。这点java就简化多了,除了静态成员包括 staic final在方法区上(如果是引用类型的话对象本身任然创建在堆上)其余都创建在堆上,而且存储空间的分配由系统自动搞定且销毁也由gc搞定。c++就麻烦多了,对象和对象成员可以分别自由确定存储位置,存储位置的不确定性意味着销毁也得自己搞定,同时这也意味着有提升程序效率的空间了。
c++中的引用是这个东西Student& s2= Student("abc");这个s2就是一个引用,其实质就是一个别名,其实现是通过指针,但比指针安全多了,而且c++的引用和常量一样在初始化时必须绑定,且只能绑定一次。c++这个引用比起java的引用变量就弱多了,谁叫c++把类类型的对象的定义与初始化放一起的呢。可以看出,java的引用类型与基本类型是完全不同的,是严格区分对象的创建和对象的引用的。c++对类类型的处理仅当作一种用户自定义数据类型对待,依然遵循基本类型的创建方式并可以重载操作符等。这点与java这种纯面向对象的是根本上的不同。也难怪c++是从c发展以兼容方式过度到面向对象的,而java直接汲取这些经验另行起家就少了太多这些包袱了。
【2】、程序结构
Java中一切类,小程序是类,大程序也是不同类以一定层次结构组成。当类多了呢?就有了包结构。不同的包可以有同名的类,每一个类加上完整的包路径可以唯一确定,这点保证了项目规模大了后不会产生类名冲突。Java程序在设计时,一般都面向接口、面向抽象编程,这样可扩展性、和可维护性才好,故会先设计出接口和抽象类,划分出包结构层次。接下来以包为单位分发下去,可以由不同程序员或小组分工实现,面向对象的协作开发优势就体现出来了。高度封装的包很好的让代码可重用、也有很多第三方包;封装、接口的约束让重构也方便、安全得多。
C++优势是既可以面向过程、又可以面向对象,但面向过程和面向对象是两种不同的思想,过程化的一面以效率、硬件操作等为优势,对象化的一面以可扩展性、可维护性等为优点,本身就鱼与熊掌不可兼得。但同时比起java等纯面向对象语言c++又要体现出自己的即可面向过程又可面向对象的优势,也难怪c++程序员入职要求高了。粗浅总结各路大佬们的看法,第一、c++毕竟不是c,构建的程序是需要一定的可扩展性、可维护性的。第二、兼容c本身又是一种优势,所以当性能成为程序瓶颈时,可以适当的重构,但最好也要把趋向过程的代码分离出来。
C++程序文件:头文件、实现文件、功能文件
头文件得作用是给源程序提供可以使用得外部资源一览表,多数为声明,而相应得资源定义在其他实现文件中。其他文件使用头文件时用预编译指令“#”包含进去。像函数这种虽然只能定义一次,但可以多次声明(extern),在文件中声明了却不调用,即不增加开销,也不算错误。因而头文件不仅仅包括函数声明,还包括:
全局变量声明: 如extern int n;
类类型的声明: 如class A;
全局函数声明: 如void fn(); //前面三个都是声明,不是定义
全局常量定义: 如const double pi=3.14;
枚举的定义: 如enum COLOR{…};
模板声明与定义:如template<class T> class A{…};
还有、、、
名空间定义: 如namespace N{…};
预编译指令: 如#include<iostream>
注释: 如//哈哈
、、、
C++在编译程序之前,应#include指令,先将头文件得内容展开,作为程序声明或定义的一部分,然后再编译,最后才连接库文件成可运行程序。
实现文件,多为全局变量定义、函数定义、类定义,他们属于外部链接,被用来在其他文件使用,故遵循一次定义原则,不能放在头文件中。其他都是在自己的文件内使用,故可以跨文件重复定义,又称内部链接。如全局常量,声明定义在头文件中,包含头文件的文件都有一致的定义,没问题。
功能文件,这个是相对的了,但多数都用来包含头文件、使用实现文件,自己做一个功能集中的操作。比如一个小的功能模块中,可能有很多文件,但最终提供给外界的功能接口就那么几个文件,这几个文件就是功能文件,使用的就是头文件和一些声明的实现文件,而自己给外部模块提供功能调用。
名空间:早期采用多文件结构,但随着程序规模的增大,全局名的冲突依然愈演愈烈,名空间就是为了更大规模的程序设计而提出。名空间是比文件更大的组织单位,能更好的应对程序结构组织,更好的划分模块。
所有c++标准库都属于名空间std,在使用标准库时,可以一次列出好几个标准库的头文件,然后用using namespace std;默认导入。还可以局部默认名空间使用,特别是有同名资源时,如using std::cout;
如:一种面向过程的文件、模块组织方式
模块一
模块二 模块三
面向对象文件的基本结构:
1、 类定义的头文件:
#include<iostream>
using namespase std;
class Student{
int id;
public:
Student(int i=1){};
int getId();
}
2、 类实现文件:
#include”student.h”
Student(int i=1){
id = i;
}
int getId(){
return i;
}
3、 功能文件:
#include<iostream>
#include”student.h”
using namespace std;
int main(){
Student s;
cout<<s.getId();
return 0;
}
全局变量的问题:全局变量往往牵扯过多的文件、模块,增加了模块间的耦合度,能传参就传参吧、能用类的静态成员就用吧,尽量少用全局变量。和全局变量一样,所有的全局对象在主函数(main)调用前,全部被构造好了(其构造顺序也是不确定的)。这样一来程序调试也受影响,主函数开始之前就初始化了全局变量及对象,因此在捕捉异常前,错误就可能产生,所以可能还没来得及取得程序的控制权程序就死了。和慎用全局变量一样,少用或不用全局对象。
【3】、对static的含义
Java的staic:修饰引用、变量、方法则定义静态成员。引用变量与普通变量存储到方法区(对象万年不变在堆)。修饰类则该类做嵌套类。与c++很不同的是,static还涉及到类的加载、成员初始化顺序问题。(我的另一篇总结)
C++中的static在不同情形有不同的含义,大致分为:
面向过程的static:全局变量或函数加static限制作用域在本文件内
局部变量加static将存储位置改为全局区,生命周期为整个程序。其初始化是第一次在函数中被执行到时,且仅初始化一次。
面向对象的类的static成员:特别之处在于static成员变量不能出现在构造函数中,通常在实现文件中初始化。static成员函数在类外部实现时不用加static关键字。
C++能用static变量就别用全局变量,其一避免全局名字冲突的可能性;其二安全,仅本类访问。静态数据成员可以是private成员,而全局变量不能。
总结:java与c++一样把类的static成员作为类所有,static函数都不能访问非static成员,因为其初始化在所有对象初始化之前(仅初始化一次)。为了保证这点,java中static成员变量自动初始化在第一个对象初始化前(链接阶段分配static变量的空间,再初始化)。C++中是靠程序员自己在实现文件中初始化的。
【4】、final与const
Java的final:用final修饰引用,引用变量不可改变指向,故真正定义常量时用static final一起,且常量只能在定义时初始化,不能像c++可以在初始化列表中为每个对象分别指定const变量的初始值。final修饰类则该类不能被继承。fianl修饰方法则该方法不能被重写。还有一点是,用类引用static fianl修饰的成员变量不会触发类的初始化,因为这是一个经过编译优化的常量池数据(static fianl Student = new Student()这种不算)。
C++的const:
1)、定义常量包括常对象。const定义的常量有类型检查、调试的便利,且编译时有优化,故应该代替宏常量的使用。特别的是常对象不同于java的final,它的所有成员都是不可更改的,而且常对象不能调用非常成员函数。
2)、const修饰普通函数的参数或返回值,只读不可写。
3)、面向对象中,类的常成员只能通过构造函数的参数列表初始化,且非常成员函数不能访问常成员。但常成员函数对所有成员都可以访问,故一般成员函数能加const的尽量加。还有重要的一点是,有无const的两个同名函数视为重载函数,这在声明与实现时要特别注意。
【4】、类结构
Java的类定义与实现作为整体在一起,至于设计层次的事交给接口和抽象类。c++可以把类的定义与实现分离开,便于搞类设计的程序员与做类实现的程序员分工。但也可以把定义与实现合在一起,不过这样的话静态成员变量就麻烦了,它必须单独于定义文件初始化。
开”后门”的友元:java遵循严格的private、默认、protect、public的访问控制,外部要访问私有成员只能通过提供的接口间接访问。这样的一致访问让类更符合封装的要求也更安全。但若有这么一个类,它的私有成员需要频繁地被普通函数、其他类的某一函数、甚至其它类的全部函数访问,处于某种原因这个类又不能提高访问权限,这时这种访问的间接性严重拖慢程序效率。所以c++提供友元机制,让外面的函数、类可以直接访问私有成员。专业一点:友元函数(包括普通函数和类成员函数)、友元类。C++的友元因该做为一种提高程序性能的后备方案,避免一开始就加入友元或滥用友元,它到底还是破坏了类的封装,安全性是一方面,可维护性和可扩展性也是一个考虑点。友元再次体现c++宁愿增加一些不稳定因素也要提高程序性能、效率的追求。程序员若能利用好这些“不稳定因素”那么自然让程序有更多发挥空间,java就别想了,什么都想抓哪有这样好事。
访问权限:Java的类可以用默认访问权限(通常为本模块public类提供功能,不对外),也可以用public访问权限,甚至可以static修饰做嵌套类(如LinkedList集合中的节点类就是了)。类成员:默认包访问权限,private私有,protected除了包访问继承类也可以,public修饰通常做对外公共接口。
C++有名空间,但名空间只作全局上的程序结构组织,不过多与访问修饰符掺和到一起。比如c++中的protected做的仅是在private的访问基础上让继承类有访问权限,这与java是不同的。C++类都不加修饰符的,类成员默认private、扩展到子类protected、公共接口public。在继承时根据访问修饰符还可以从整体上调整访问控制,不过只能整体向下调整,如class Student:protected Man{}这样将父类原本是public的成员以protected继承,而对于private的依然还是以private继承。只能整体向下调整,不能提高。这样的结果是,对于外面访问Student的类来说,它的所有继承自父类的成员都是不可见的包括父类的public成员。前面之所以强调是继承时整体向下调整,是因为继承后还可以做局部的调整。如class Student:protected Man{public:using Man::id;}这样相比前面,外面访问Student时又可以访问id这个Student从父类继承的成员了。
总结:java成员乃至类都默认包访问权限,可见包对java程序组织的重要性,protected不仅有包访问权限还扩展到了子类。事实上,java文件虽然全是类,但从程序整体看包才是程序的组织单位。一个模块是一个包,内部不管怎么构造类实现,最终遵从接口和抽象类的约束对外提供public访问接口。也就是说内部功能具体实现可以优化、可以扩展,但还是遵从接口和抽象类的约束对外提供public访问入口,一来功能模块的实现自由性大,二来可维护也可扩展,只要遵从对外接口,内部变动不影响其他模块。传说中的高类聚、低耦合?c++虽然有名空间,但只从全局上做一个约束,类成员默认访问权限还是private,因为名空间不搅合访问控制,只要导入相应名空间,里面成员都可以使用。故c++在继承时让子类可以以不同访问权限方式继承父类成员,以此加强自己的访问管理进一步约束外部类的访问。想一致性的兼容c不付出代价怎么行?
【5】、对象的创建与内存释放
(1)、类的默认方法或函数:
Java的类只有一个默认的无参构造方法,可以根据需要自定义带默认值的无参构造方法,和其他基本类型或引用类型参数的构造方法来初始化对象。
C++有五类默认函数:默认的无参构造函数、默认的复制函数、默认的赋值运算符函数、默认取址运算符函数、默认的析构函数。
Empty(); // 缺省构造函数
Empty( const Empty& ); // 复制构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符函数
Empty* operator&(); // 取址运算符函数
const Empty* operator&() const; // 取址运算符函数 const
如同普通函数一样,构造函数也可以带默认值。Student(int i=1):id(i){ }; 还有初始化列表,常量和引用类型成员的初始化必须用初始化列表,而静态变量初始化要在类声明文件外。
构造函数的结果:java返回初始化后对象的引用,c++就是初始化后的对象。
(2)、创建对象的方式:
java创建对象的方式有大致4种:new、反射(class或constructor)、clone、反序列化。前两种创建对象需要调用构造器,后两种不用。不论哪种都严格区分对象的创建与对象的引用。一旦对象失去所有引用就沦为“垃圾”等待gc下次执行垃圾回收时的自动回收,对象销毁与程序员并没有多大关系。
Student student = new Student();
|
|
|
new Student() |
|
栈 |
|
堆 |
student
c++创建对象的方式大致分三种:
Student a(1); //栈中分配
Student b = Student (1); //栈中分配
Student * c = new Student (1); //堆中分配
前两种没什么区别,一个隐式、一个显示调用构造器,且默认采用栈上分配内存。栈上资源的分配与释放都由系统管理。第三种是程序员自行在堆中分配内存,销毁也要自己delete[],用一个指向该类型对象的指针操作。
主要区别是,前两种对象在栈上分配内存,其成员的空间大小在编译时就确定的,再加上计算机在底层对栈提供的支持,其效率很高,但操作系统给每个进程分配的栈大小是有限制的。当对象的成员数据很大时就不适合分配在栈上了,或者当该对象的组合成员也是类类型的时候,因为其成员大小需要运行时确定,分配在栈空间上那么编译时给多大空间?空间利用率堪忧。另一个问题是,生存周期不同,前两种在超出生命周期后(函数返回后)其栈空间被系统收回(类类型会按出栈顺序调用析构函数,),第三种因为分配在堆上,只要不主动销毁会生存直到内存泄漏。(内存泄漏出现在堆,但导致的原因可能和栈,全局区、堆有关)
再次强调前面的,c++的对象也是当作一种变量,这个变量不仅可以重载普通变量的一些运算符,还可以自定义成员集合和运算集合。这非常区别java的引用类型和引用变量机制。Student a[3];//调用三次默认构造函数构造3个student类型的3个对象组成的数组。
Student b[3]={Student(2)};//调用一次有参构造函数,2次无参构造函数。
(3)、创建对象的过程:
Java中分加载、链接、初始化。(我的另一篇总结)
C++中: 1.分配内存空间
2.初始化成员变量
3.执行构造方法
第一步分配内存空间时,Student a(1); Student b = Student (1); //前两种栈中分配,Student * c = new Student (1); //堆中分配。
第二步按成员变量的声明顺序初始化,其中构造参数列表的存在,可以指定初始化成员变量的构造方法,特别是对类类型的组合成员了。
最后才执行构造方法中的语句。基本数据类型的赋值,指针类型成员的动态内存开辟等。
若含父类,按上面2、3步骤先递归创建父类(组合和继承关系中,子类都显示、隐士包含父类成员)。
(4)、C++特有的复制构造函数和赋值运算符函数:
前面提到了c++的三种创建对象的方式。还有一种特别的初始化一个对象的方式,即用一个已经存在的相同类型的对象来初始化一个对象。C++把他叫“复制构造函数”并为每一个类确定一个默认的复制构造函数(浅复制)。与此对应的是用户自定义的复制构造函数(深复制)。
个人觉得没必要把“复制构造函数”确立为默认函数,从而衍生出一个“浅复制”与“深复制”的反义词出来。一来构造函数就是用来创建对象的,至于用什么类型的参数来初始化是用户的自由,如java般简洁就好,用户需要同类型的对象来初始化就自己定义呗。其二浅复制反而增加了编码的混淆。看下面这个:
#include<iostream>
#include<string>
using namespace std;
class Product
{
char* data;
public:
Product(char* d)
{
data = new char[strlen(d) + 1];
strcpy_s(data, strlen(d)+1,d);
}
Product(const Product& p) //复制构造函数
{
data = new char[strlen(p.data) + 1];
strcpy_s(data, strlen(p.data) + 1, p.data);
}
void changeDate(char* d)
{
strcpy_s(data, strlen(d)+1, d);
}
void display()
{
cout << data << endl;
}
~Product() //析构函数
{
delete[] data;
}
};
int main()
{
Product p1("1000 - 01 - 01"), p2(p1);//用p1的状态复制初始化p2
p1.display(); p2.display();
p1.changeDate("2000 - 02 - 02");//改变p1的日期
p1.display(); p2.display();
getchar();
}
本意是用p1产品的值初始化p2产品,改变p1的data应该不影响p2,然而输出:
结果确是p2也跟着一起变了。因为“浅复制”是简单的值传递,而程序中的data是地址(本质也是整型值)也简单传递了。也就是说浅复制不适用于动态内存分配的对象。
其实用户要用同类型对象初始化一个对象完全可以自己重载构造函数。偏偏弄一些概念出来强行增加难度。还有的书上又丫的来个“用于转型的构造函数”,不就是用其他不同类对象来初始化对象吗?强行搞事啊!
关于赋值运算符函数:就是把一个对象的状态复制给另一个同类型的对象。与浅、深复制含义不同的是,赋值通常是指两个已经存在的同类对象通过赋值表达式赋值,而浅、深复制强调在对象初始化时。这也就是说赋值,需要释放左值的内存资源(只针对其中动态内存分配的部分成员),而浅、深复制因为本身就是初始化就免了这一步。
C++的对象也是变量,所以也可以加减等运算,不过需要自己写这些运算符的重载函数,如:Product& operator= (Product& p){
if(this==&p) return p;//是自己就直接返回
delete[] data;//先释放资源,理由是上次动态分配的大小可能不够存放这次要复制的动态大小了
data = new char[strlen(p.data)+1];//还有字符’\0’
if(data) strcpy(data,p.data);
return *this;//本质是赋值
}
重载运算符进行赋值的方式完全可以用函数完成,如Product& doCopy(Product& p);包括其他运算符也是一样。就为了一个所谓“=”的人性化,就又特殊出一个赋值操作。而且与构造复制一样,赋值也分“浅赋值”与“深赋值”,浅赋值也是默认有的赋值符“=”,浅赋值也不适用于动态内存分配的对象。
如上面例子的main函数改为:
int main()
{
Product p1("1000-01-01"), p2("2000-02-02");
p1.display(); p2.display();
p1 = p2;//p2的值赋给p1
p2.changeDate("3000-03-03");//改变p2
p1.display(); p2.display();
getchar();
}
输出 显然赋值后,p1还是随p2变化,这就与赋值的初衷相违背了。
有了所谓的复制构造函数和赋值运算符函数的搅合,就有了下面创建对象的情况:
int main()
{
//第一种创建对象的情况
Product p1;//默认构造
Product p2("1000-01-01");//含参构造
Product p3 = "3000-03-03";//含参构造(本来是用含参构造临时对象+复制构造,不过被编译器优化后就是用的含参构造了)
//第二种创建对象的方式
Product p7 = Product(p2);//复制构造
Product p5(p2);//复制构造
Product p4 = p2;//复制构造
//特殊的
Product p6;//默认构造
p6 = Product("6000-06-06");//含参构造(一个临时对象,过后被析构) + 赋值函数
}
而且还要注意的是复制与函数的纠纷,即类对象在函数中选择不同传参方式、返回方式带来的问题。用传值或返回值的方式,那么会自动调用复制构造函数建临时对象,这时如果这个对象有动态内存分配的成员又没自定义这个复制构造函数,那么用默认复制构造函数,然后就出事了。(出事不是说结果一定有问题,而是复制与被复制的类动态分配成员处于同一内存空间,一个的变动会不合理地影响另一个,更可怕的是内存泄漏,对同一内存空间的重复释放也可能导致堆崩溃。)
如,上面例子改添个函数:
Product do1(Product p)
{
p.changeDate("2000-02-02");
return p;
}
int main()
{
Product p1("1000-01-01");
p1.display();
Product p2 = do1(p1);//p2.date值"2000-02-02"
p2.display();
p1.changeDate("3000-03-03");//p1的改变又影响了p2,因为他们不是正确的值的复制
p2.display();
getchar();
}
函数传值、返回值时会去调用复制构造函数,这样产生了两个临时对象,如这个对象有动态内存分配的成员又没自定义这个复制构造函数就与“复制”的目的相悖。而且上面调用函数do1()过程产生了两次内存泄漏,哪两次?
总结:java因为引用类型与引用变量的存在,在赋值、函数传参与返回时都默认传递引用,很多事都给系统去处理了,故默认函数就一个无参的构造函数。而c++它本质上把对象也当普通变量处理,那么变量都是可以函数传参与返回、赋值操作的。为了保证变量基本操作的一致性以及对c的兼容,如果不提供默认的复制构造函数,那么对象这个有点特别的变量在函数传参与返回时怎么办?故它勉为其难的提供了默认复制构造函数。如果它不提供默认的赋值运算符函数,那么对象这个有点特别的变量要赋值怎么办?故它勉为其难的提供了默认的赋值运算符函数。而当我们自定义了复制构造函数和赋值运算符函数就会覆盖默认的,这个操作手法上有点像java的toString()方法,需要的时候系统会自动调用完成功能,但这是个通用处理往往需要我们自行覆盖实现。如果程序员忽略了这几个默认函数的有限功能,操作的对象本身又含动态内存分配,就可能会有产生超乎预料的结果,更可怕的是因为对同一内存空间的重复释放可能导致堆崩溃,内存泄漏就变得理所当然了。C++之所以搞出这些让问题有点繁杂,特别是与函数、赋值搅一起了,根本上还是它让程序员自行决定类对象成员的内存分配位置,为了效率确实让编程比java等复杂多了。
(5)、c++特有的析构函数:
主要是用在对象消亡后的资源扫尾工作,但它不是必须的,与对象含有的成员类型也无关,如果一个对象的所有成员都分配在栈上或全局区上,那么并不一定需要析构函数,因为栈上的变量在超出作用域后会出栈被系统回收,全局区的资源在结束程序时也被系统回收。析构函数用在动态内存的释放,即堆上的数据。需要用到析构函数的情景:栈上对象有成员动态分配在堆上;全局区对象有成员动态分配在堆上;直接用new动态分配在堆上的对象、、、一句话动态内存的分配都需要主动回收,回收多通过析构函数,故含动态内存分配的对象一定要定义好析构函数。
内存泄漏情形:
1、 对象有成员动态分配在堆上没正确析构释放。
2、 指针赋值,首先判断被赋值指针是否为空、再释放内存、赋值。
3、 指针容器,如vector<Ctype*>,在删除元素或clear前要释放相应的内存。
4、 对象指针数组要用delete[],用delete只释放数组成员,不会调用成员指向的对象的析构函数。
5、 虚析构函数,将基类的析构函数定义成虚函数,这样做是为了当用一个基类的指针类型来删除一个派生类的对象时能见效。
6、 有动态内存分配的对象缺少复制构造函数,又进行函数传参、返回时。
7、 有动态内存分配的对象缺少赋值运算符函数,又进行赋值时。
5000空间会内存泄漏,P1、p2重复释放6000空间可能造成堆崩溃。
【6】、继承中的问题
Java严格默认从Object单根继承(导致所有类都直接或间接继承自object),作为单根继承不足的弥补,可以多接口实现,还有就是组合等编程技巧(23种设计模式啊!!!)。
C++默认木有父类,想轻装上阵?没问题。想重装出发?没问题,还可以多继承。不过多继承看起来很丰富,但程序复杂度高,不仅存在父类同名成员二义性问题(不仅父类,还有父类具有相同父类时的冲突),还有多态操作时的向上转型模糊问题。
多继承的两种基本形式:
|
这种可以用声明为虚基类,共享它的父类给子类,减少开销和冲突。 |
总结:java就是面向对象的,别说什么它还有基本数据类型不算完全面向对象。继承使得类具有层次结构,但c++不是必须具有层次结构不可,比如仅仅把类看做带函数绑定的结构体,自定义数据类型的一种方式,故有的书上取名“基于对象编程”。一句话,c++类层次结构的可选择性让程序规模大小皆宜,考虑运行效率是它的优势;java的本质是让程序员专注于解决问题本身,而不是去纠结底层运行效率(看来得抽空瞧瞧python)。
C++继承中还有特别的访问权限控制(看上面)
【7】、其他
c++中表达式也可以作为bool判断,0和NULL为false,其他为true,bool结果为0或1。java的bool判断只能是一个严格的bool值,bool结果为ture或false。
C++函数(包括构造函数)声明时,可以带默认参数。
C++的switch语句的条件可以是整型(包括字符型)和枚举型。Java中还可以是字符串。
Java文件都是类,产生的全是对象,属性与对属性操作的方法都是一个整体,故没有c/c++中的全局区。大致分栈、堆,堆中就包括了方法区、常量区、创建对象的动态区等。更大的区别是,java所谓的栈、堆都是处于java虚拟机虚拟出来的内存空间上,它的栈操作需要由虚拟机解释到计算机的栈上。而真正的内存大致分为:系统区、栈区、用户区。对于系统区用户进程就别想掺和了,栈区是执行二进制指令的公共区域,所有进程都可以声请,操作系统提供底层支持且掌管分配与回收,就是进出栈操作,效率远不是用户区可以比的。用户区由进程自由申请使用,也就是说只简单记录哪些区域闲置然后分给申请进程,若进程使用了后不归还它也不管,c/c++内存泄漏的大致由来。从此看出,c/c++所说的堆是指可以在用户区申请到的动态内存空间,并没有什么内存结构,简单存取且效率相对低,但java因为对象全在虚拟机的逻辑堆中,故java对这个“堆”有优化(了解不多),但可以肯定的是因为虚拟机的约束又没指针,顶多搞崩虚拟机,这要比c++安全多了同时运行效率低了(可否加个多字?)。
C++可以用const声明常成员函数,即函数中的参数只能访问,不能写入。主要用处在于软件设计时确保程序质量。
Java的new与c++的new操作符区别也大。他们相同的一点是都隐式的申明为static。C++单纯多了,仅提供一个在堆中动态分配内存的方式并返回首地址指针,然后啥都看程序员自己的了。而Java的new不仅仅表示创建对象并返回引用,它也是java的类从加载、链接到类与对象初始化顺序的正确保障的机制,更是static成员变量初始化与继承结构正确建立的一种保障。
Java所有对象不论大小都创建在堆,默认单继承object的所有成员,也就是说随便创建个空对象在java类机制的重重“外衣”下都显得“重量级”。C++默认没继承,而且对于类默认的函数若是没调用也不会一股脑加进去,足够轻但靠程序员做的内容也就多了。C++还可以自由确定对象(包括对象的成员)的创建位置,比如把能确定大小范围的字符串成员用字符数组存在栈(有风险),以此提高运行效率,但若字符串变动范围大或大小不确定又可以创建动态内存来提高内存利用率。C++的可选择性比java大多了,但也复杂多了,对程序员要求也就高了。
浙公网安备 33010602011771号