一、strcpy函数的编写?(memcpy函数的编写)
1)函数原型:extern char *strcpy(char *dest,const char *src);
功能:将src所指由NULL结束的字符串复制到dest所指的数组中。位于string.h头文件里。
注意:src和dest所指内存区域不可重叠,且dest必须有足够空间来容纳src的字符串;
src字符串尾结束标识符‘\0’也会被复制过去。
接下来讨论:
(1)实现strcpy函数。
(2)解释为什么要返回char *。
(3)假如考虑dest和src内存重叠的情况,strcpy该怎么实现。
(1)实现strcpy函数
char * strcpy(char *dest,const char *src)
{
assert((dest!=NULL)&&(src!=NULL));
char *result=dest;
while((*dest++=*src++)!='\0');
return result;
}
(2)[1]const修饰:源字符串参数用const修饰,防止修改源字符串。
[2]空指针检查
(A)不检查指针的有效性,说明答题者不注重代码的健壮性。
(B)检查指针的有效性时使用assert(!dest&& !src);
char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。
(C)检查指针的有效性时使用assert(dest!= 0 && src != 0);
直接使用常量(如本例中的0)会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。
[3]返回目标地址
(A)忘记保存原始的strdst值。
[4]'\0'
(A)循环写成while(*dst++=*src++);明显是错误的。
(B)循环写成while(*src!='\0') *dst++=*src++;
循环体结束后,dst字符串的末尾没有正确地加上'\0。
2.为什么要返回char *?
返回dst的原始值使函数能够支持链式表达式。
链式表达式的形式如:
int l=strlen(strcpy(strA,strB));
又如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。
其一,源字符串肯定是已知的,返回它没有意义。
其二,不能支持形如第二例的表达式。
其三,把const char *作为char *返回,类型不符,编译报错。
3.假如考虑dst和src内存重叠的情况,strcpy该怎么实现
char s[10]="hello";
strcpy(s, s+1); //应返回ello,
strcpy(s+1, s); //应返回hhello,但实际会报错,因为dst与src重叠了,把'\0'覆盖了
2)函数原型:void *memcpy(void*dest, const void *src, size_t n);
用法:#include<string.h>
功能:从源src所指的内存地址的起始位置开始,拷贝n个字节的数据到目标dest所指的内存地址的起始位置中。
说明:
1)src和dest所指内存区域不能重叠,函数返回指向dest的指针。如果src和dest以任何形式出现了重叠,它的结果是未定义的。
2)与strcpy相比,memcpy遇到’\0’不结束,而且一定会复制完n个字节。只要保证src开始有n字节的有效数据,dest开始有n字节内存空间就行。
3)如果目标数组本身已有数据,执行memcpy之后,将覆盖原有数据(最多覆盖n个)。
如果要追加数据,则每次执行memcpy()后,要将目标地址增加到要追加数据的地址。
4)source和destin都不一定是数组,任意的可读写的空间均可。
实现memcpy库函数:
void *memory(void *dst,const void *src,size_t s)
{
const char* psrc=static_cast<const char*>(src);
char* pdst=static_cast<char*>(dst);
if(psrc==NULL||pdst==NULL)
return NULL;
if(pdst>psrc&&pdst<(psrc+s))
{
for(size_t i=s-1;i!=-1;i--)
pdst[i]=psrc[i];
}
else
{
for(size_t i=0;i<s;++i)
pdst[i]=psrc[i];
}
return dst;
}
二、数据结构中二叉树的非递归遍历(画图举例讲解,写代码)
前序遍历:
//前序遍历的非递归遍历
void preOrder2(BinaryTreeNode* pRoot)
{
stack<BinaryTreeNode*> s;
BinaryTreeNode *p=pRoot;
if(pRoot==NULL)
return;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
cout<<p->value<<" ";
s.push(p);
p=p->left;
}
if(!s.empty())
{
p=s.top();
s.pop();
p=p->right;
}
}
}
中序遍历:
//中序遍历的非递归法
void inOrder(BinaryTreeNode* pRoot)
{ stack<BinaryTreeNode*> s;
BinaryTreeNode *p=pRoot;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
s.push(p);
p=p->left;
}
if(!s.empty())
{
p=s.top();
cout<<p->value;
s.pop();
p=p->right;
}
}
}
后序遍历:
//后序遍历的非递归法
void postOrder(BinaryTreeNode* pRoot)
{
stack<BinaryTreeNode*> s;
BinaryTreeNode *cur;
BinaryTreeNode *pre=NULL;
s.push(pRoot);//根结点入栈
while(!s.empty())
{
cur=s.top();
if((cur->left==NULL&&cur->right==NULL)||(pre!=NULL&&(pre==cur->left||pre==cur->right)))
{
//左孩子和右孩子同时为空,或者当前结点的左孩子或右孩子已经遍历过了
cout<<cur->value<<" ";
s.pop();
pre=cur;
}
else
{
if(cur->right!=NULL)
s.push(cur->right);
if(cur->left!=NULL)
s.push(cur->left);
}
}
}
三、c++中四种类型转换机制。(前面已经总结过)
四、继承机制中对象之间是如何转换的?
向上转型:向上转型是隐式转换。
将子类对象看作是父类对象;
解释:平行四边形是四边形的一种,那么就可以将平行四边形对象看作是一个四边形对象。例如:鸡是家禽的一种,而家禽是动物中的一类,那么也可以将鸡对象看作是一个动物对象。
package ten;
class Quadrange{//四边形类
public static void draw(Quadrange q){//四边形类中的方法
}
}
public class Parallelogram extends Quadrange{//平行四边形类,继承了四边形类
public static void main(String[] args){
Parallelogram p=new Parallelogram();//实例化平行四边形类对象引用
draw(p);//调用父类方法
}
}
四边形类存在一个draw()方法,它的参数是Quadrange(四边形类)类型,而在平行四边形类的主方法中调用draw()时给予的参数类型是Parallelogram(平行四边形类)类型的。这就相当于“Quadrange obj=new Parallelogram();”就是把子类对象赋值给父类类型的变量。
试想一下正方形类对象可以作为draw()方法的参数,梯形类对象同样也可以作为draw()方法的参数,如果在四边形类的draw()方法中根据不同的图形对象设置不同的处理,就可以做到在父类中定义一个方法完成各个子类的功能,这样可以使同一份代码毫无差别地运用到不同类型之上,这就是多态机制的基本思想。
向下转型:
将抽象类转换为较具体的类,这样的转型通常会出现问题,例如,不能说四边形是平行四边形的一种、所有的鸟都是鸽子等。可以说子类对象总是父类的一个实例,但父类对象不一定是子类的实例。
package ten;
class Quadrange{
public static void draw(Quadrange q){
}
}
public class Parallelogram2 extends Quadrange {
public static void main(String[] args){
draw(new Parallelogram());//将平行四边形对象看作是四边形队形,称为向上转型操作
//向下转型
Quadrange q=new Parallelogram();
//Parallelogram p=q;将父类对象赋予子类对象,这种写法是错误的
Parallelogram p=(Parallelogram) q;//将父类对象赋予子类对象,并强制转换为子类型,这种写法是正确的
}
}
//备注:如果将父类对象对象直接赋予子类,会发生编译错误,因为父类对象不一定是子类的实例,例如:一个四边形不一定就是指平行四边形,它也许是梯形,也许是正方形等。
五、继承机制中,引用和指针之间是如何转换的?
六、虚函数,虚函数表里面内存如何分配的?(这个考前看过了,答的还不错)
七、如何实现只能动态分配类对象,不能定义类对象?(这个牛客上的题目,我把如何只能动态分配和只能静态分配都讲了一下)
http://blog.csdn.net/u010889616/article/details/48649325
动态分配类对象:就是用运算符new来创建一个类的对象,在堆上分配内存。
静态分配类对象:就是A a,这样由编译器来创建一个对象,在栈上分配内存。
1)动态分配(在堆上分配内存)
将类的构造函数和析构函数设为protected属性,这样类对象不能访问,但是派生类能够继承,也能够访问。同时,创建另外两个create和destroy函数,用于类创建对象。(将create设为static原因是:创建对象的时候,A *p=A::create();只有静态成员函数才能够通过类名来访问)。
Class A
{
protected:
A(){}
~A(){}
public:
static A* create()
{ return new A(); }
void destroy()
{ delete this; }
};
2)静态分配类对象:
把new、delete运算符重载为private属性就可以了
class A
{
private:
void* operator new(size_t t){} //函数的第一个参数与返回值都是固定的
void operator delete(void *ptr){} //重载了new,就需要重载delete
public:
A(){}
~A(){}
};
八、STL有哪些容器,对比vector和set?
1)STL容器分为顺序容器和关联容器。
顺序容器主要有vector、list、deque、string、array等。其中vector表示一段连续的内存,基于数组实现,list表示非连续的内存,基于链表实现,deque与vector类似,但是对首元素提供插入和删除的双向支持。
关联容器主要有map、multimap、unordered_map、set、multiset、unorderd_set等。map是key-value形式,set是单值。map和set只能存放唯一的key,multimap和multiset可以存放多个相同的key。
2)首先,vector是顺序容器,而set是关联式容器;
set包含0个或多个不重复不排序的元素。也就是说set能够保证它里面所有的元素都是不重复的;
另外,对set容器进行插入时可以指定插入位置或者不指定插入位置。如insert(v.begin(),1),也可以直接用insert(1)。还有一点是set对一些操作符没有进行重载,如<。
3)vector和deque的区别主要在于他们底层的实现不同,特别是在插入和删除操作的实现机制不同。
对于vector来说,不管其大小是多少,在头部插入的效率总是比在尾部插入的效率低。在尾部插入将耗费固定的时间。在头部进行插入时,耗费的时间与vector的大小成正比,vector越大,耗费的时间越多。例如,在一个大小为1000的vector头部插入一个元素,与在一个大小为10的vector头部插入一个元素相比,将耗费100倍的时间。删除操作的情形也与插入类似。因此,vector适合于插入和删除操作都在尾部进行的情况。
deque和vector不同,不管进行的插入还是删除操作,也不管这些操作时在头部还是尾部进行,算法的效率是固定的。例如:不管deque的大小是10,100,还是1000.deque在头部和尾部插入删除的时间是一样的。因此要在对于两端进行插入或者删除操作时。deque要优于vector。
九、 红黑树的定义和解释?
1)红黑树(RBT)的定义:它或者是一颗空树,或是具有一下性质的对称平衡二叉查找树:
1.节点非红即黑。
2.根节点是黑色。
3.所有NULL结点称为叶子节点,且认为颜色为黑。
4.所有红节点的子节点都为黑色。
5.从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点。
2)即该树上的最长路径不可能会大于2倍最短路径。为什么?
因为第1条该树上的节点非红即黑,由于第4条该树上不允许存在两个连续的红节点,那么对于从一个节点到其叶子节点的一条最长的路径一定是红黑交错的,那么最短路径一定是纯黑色的节点;而又第5条从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点,这么来说最长路径上的黑节点的数目和最短路径上的黑节点的数目相等!而又第2条根结点为黑、第3条叶子节点是黑,那么可知:最长路径<=2*最短路径。
一颗二叉树的平衡性能越好,那么它的效率越高!
3)红黑树放松了对平衡的限制。可以不再是严格意义上的平衡二叉树。但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
【解释】:红黑树属于平衡二叉树。说它不严格是因为它不是严格控制左、右子树高度或节点数之差小于等于1。但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log(n),这有数学证明。所以它算平衡树,只是不严格。不过严格与否并不影响数据结构的复杂度。红黑树多用于系统底层。
红黑树与AVL树的相同点与区别?
【解读】:红黑树和之前所讲的AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。自从红黑树出来后,AVL树就被放到了博物馆里,据说是红黑树有更好的效率,更高的统计性能。
红黑树和AVL树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。AVL树的复杂比起红黑树来说简直是小巫见大巫。红黑树是真正的变态级数据结构。
4)红黑树的时间复杂度:
1.在一棵二分查找树上,执行查找、插入、删除等操作的最好情况时间复杂度为O(lgn)。因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn)。
2.但若是一棵具有n个结点的线性链,则此些操作最坏情况运行时间为O(n)。
而红黑树,能保证在最坏情况下,基本的动态几何操作的时间均为O(lgn)。
|
二分查找树(类似二分查找) |
二叉平衡树(AVL树) |
红黑树 |
最坏时间复杂度 |
O(n) |
O(logn) |
O(logn) |
最好时间复杂度 |
O(logn) |
O(logn) |
O(logn) |
平均时间复杂度 |
O(logn) |
O(logn) |
O(logn) |
十、const关键字的作用?(const成员函数,函数传递和define的区别)
const成员函数:称为常成员函数,可以理解为是一个“只读函数”,它既不能更改数据成员的值,也不能调用那些能引起数据成员的值变化的成员函数,只能调用const成员函数。在成员函数的声明和定义时都需要加const修饰符;
函数传递:在函数调用时候,可以修饰函数的传递参数,防止参数被意外修改;也可以返回函数的返回值,表示返回值不可修改;
与define的区别:1)const常量有数据类型,而宏定义没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意想不到的错误(边际效应)。2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏定义进行调试。3)在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
区别:
1.内存空间的分配上。define进行宏定义的时候,不会分配内存空间,编译时会在main函数里进行替换,只是单纯的替换,不会进行任何检查,比如类型,语句结构等,即宏定义常量只是纯粹的置放关系,如#define null 0;编译器在遇到null时总是用0代替null它没有数据类型.而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查,所以const与define之间的区别在于const定义常量排除了程序之间的不安全性.
2.const常量存在于程序的数据段,#define常量存在于程序的代码段。
3. 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
十一、静态成员函数和数据成员有什么意义?
静态数据成员:类内数据成员声明的时候加上static修饰符。
特点:1)该类的所有对象共享访问该数据成员,在程序中,只有一个复制品。而对于非静态数据成员,每个类对象都有自己的复制品。
2)存储在全局数据区。定义时要分配空间,所以不能在类声明中定义。
3)与普通数据成员一样遵从public、private、protected访问规则。
4)初始化在类外,不加static。
优势:1)静态数据成员没有进入程序全局名字空间,因此不会与程序中其他全局名字冲突。
2)可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能。
静态成员函数:1)是为类的全部服务而不是为类的某个对象服务。
2)是类的内部实现,属于类定义的一部分。
3)由于不与任何对象相连系,因此不具有this指针。所以,无法访问类对象的非静态数据成员,也无法访问类非静态成员函数,只能访问静态数据成员,调用静态成员函数。
十二、模版特化的概念,为什么特化?
对模板特化的理解:
特化整体上分为全特化和偏特化,下面就谈谈我个人对特化的划分和定义:
所谓特化,就是将泛型的东西搞得具体化一些,从字面上来解释,就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,或受到特定的修饰(例如const或者摇身一变成为了指针之类,甚至是经过别的模板类包装之后的模板类型)或完全被指定了下来。
这是网上某个人的一些看法:
模板有两种特化,全特化和偏特化(局部特化)
模板函数只能全特化,没有偏特化(以后可能有)。
模板类是可以全特化和偏特化的。
全特化,就是模板中模板参数全被指定为确定的类型。
全特化也就是定义了一个全新的类型,全特化的类中的函数可以与模板类不一样。
偏特化,就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。
在类型上加上const、&、*(cosnt int、int&、int*、等等)并没有产生新的类型。只是类型被修饰了。模板在编译时,可以得到这些修饰信息。
我个人也比较赞同这位仁兄的划分,全特化的标志就是产生出完全确定的东西,而不是还需要在编译期间去搜寻适合的特化实现,貌似在我的这种理解下,全特化的东西不论是类还是函数都有这样的特点,template <>然后是完全和模板类型没有一点关系的类实现或者函数定义,如果你要说,都完全确定下来了,那还搞什么模板呀,直接定义不就完事了?但是很多时候,我们既需要一个模板能应对各种情形,又需要它对于某个特定的类型(比如bool)有着特别的处理,这中情形下就是需要的了。
既然刚才提到了全特化的标志,那么再说说其他一些共性的东西:
一个特化的模板类的标志:在定义类实现时加上了<>,比如class A<int, T>;而在定义一个模板类的时候,class A后面是没有<>的
全特化的标志:template <>然后是完全和模板类型没有一点关系的类实现或者函数定义
偏特化的标志:template <typename T.....>,是说还剩下点东西,不像全特化<>整得那么彻底。
十三、explicit是干什么用的?
当类的构造函数只有一个实参时,则定义了此类类型的隐式转换规则。在要求抑制只有一个实参的构造函数的隐式转换规则时,可以通过将构造函数声明为explicit加以阻止。
十四、strcpy返回类型是干嘛用的?
返回dst的原始值使函数能够支持链式表达式。
十五、内存溢出有那些因素?
概念:应用系统中存在无法回收的内存或使用内存过多,最终使得程序运行需要的内存大于虚拟机能提供的最大内存。
原因:1)内存中,加载的数据量过于庞大。如一次从数据库中取出过多数据。
2)集合类中有对对象的引用,使用完后未清空,使得JVW(JAVA虚拟机)不能回收。
3)代码中存在死循环或循环产生过多重复的对象实体。
4)使用的第三方软件的BUG。
5)启动参数内存值设定的过小。
解决方案:
1)修改JVW启动参数,直接增加内存。
2)检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
3)对代码进行走查和分析,找出可能发生内存溢出的位置。
十六、new与malloc的区别,delet和free的区别?
共同点:
1)都必须配对使用,这里的配对使用,可不能理解为一个new/malloc就对应一个delete/free,而是指在作用域内,new/malloc所申请的内存,必须被有效释放,否则将会导致内存泄露,至于内存泄露的检查方法,
2)都是申请动态内存,位于堆中,释放内存,free和delete可释放NULL指针。
区别:
1、malloc和free是C语言标准函数库的两个函数,位于头文件<stdlib.h>中,new/delete是C++语言中两个运算符。
3、new不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free 也不会调用析构函数。
4、malloc得到的指针void类型,new得到的指针是带有类型的。
5、new能够自动计算需要分配的内存,而malloc需要手动计算字节数。
注意:free和delete被调用后,内存不会立即被收回,指针也不会指向为空。仅仅是告诉操作系统,这一块内存被释放了,可用作其他用途。但,由于没有重新对这块内存进行写操作,所以内存中的变量值并没有变化,出现野指针的情况。因此,释放完内存后,应该将指针指向置位空。
十七、为什么要用static_cast转换而不用c语言中的转换?
static_cast类似于C风格的强制转换。无条件转换。
静态类型转换。用于:
1. 基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。(基类和子类之间的动态类型转换建议用dynamic_cast)
2. 基本数据类型转换。enum, struct, int, char, float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
3. 把空指针转换成目标类型的空指针。
4. 把任何类型的表达式转换成void类型。
5. static_cast不能去掉类型的const、volitale属性(用const_cast)。
dynamic_cast有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL):
1. 安全的基类和子类之间转换。
2. 必须要有虚函数。
3. 相同基类不同子类之间的交叉转换。但结果是NULL。
reinterpreter_cast仅仅重新解释类型,但没有进行二进制的转换:
1. 转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。
2. 在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。
3. 最普通的用途就是在函数指针类型之间进行转换。
4. 很难保证移植性。
总 结
去const属性用const_cast。
基本类型转换用static_cast。
多态类之间的类型转换用daynamic_cast。
不同类型的指针类型转换用reinterpreter_cast。
十八、必须在构造函数初始化式里进行初始化的数据成员有哪些?
构造函数中,成员变量一定要通过初始化列表来初始化的有以下几种情况:
1、const常量成员,因为常量只能在初始化,不能赋值,所以必须放在初始化列表中;
2、引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表中;
3、没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数;
十九、异常机制是怎么回事?
C++的异常处理结构为:
try
{
//可能引发异常的代码
}
catch(type_1 e)
{
// type_1类型异常处理
}
catch(type_2 e)
{
// type_2类型异常处理
}
catch (...)//会捕获所有未被捕获的异常,必须最后出现
{
}
而异常的抛出方式为使用throw(type e),try、catch和throw都是C++为处理异常而添加的关键字。
在C++中,throw抛出异常的特点有:
(1)可以抛出基本数据类型异常,如int和char等;
(2)可以抛出复杂数据类型异常,如结构体(在C++中结构体也是类)和类;
(3)C++的异常处理必须由调用者主动检查。一旦抛出异常,而程序不捕获的话,那么abort()函数就会被调用,程序被终止;
(4)可以在函数头后加throw([type-ID-list])给出异常规格,声明其能抛出什么类型的异常。type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。如果函数没有异常规格指定,则可以抛出任意类型的异常。
如果一种异常没有被指定catch模块,则将导致terminate()函数被调用,terminate()函数中会调用abort()函数来终止程序。
二十、迭代器删除元素的会发生什么?
顺序性容器:(vector和list和deque)
erase迭代器不仅使所指向被删元素的迭代器失效,而且使被删元素之后的所有迭代器失效,所以不能使用erase(iter++)的方式,但是erase的返回值为下一个有效的迭代器。
所以正确方法为:
for( iter = c.begin(); iter != c.end(); )
iter = c.erase(iter);
关联性容器:(map和set比较常用)
erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器,
所以正确方法为:
for( iter = c.begin(); iter != c.end(); )
c.erase(iter++);
Tips:其实对于list两种方式都可以正常工作。
STL的容器删除元素,除了使用迭代器外,还可以使用erase(key)的方式。
size_t rm_num = obj.erase(key);
rm_num标示删除key的成员的个数,在map中key是key值,在其他容器中,key是一个value。
二十一、auto_ptr类
auto_ptr是C++标准库中(<utility>)为了解决资源泄漏的问题提供的一个智能指针类模板(注意:这只是一种简单的智能指针)。
auto_ptr的实现原理其实就是RAII,在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。
std::auto_ptr<ClassA> pa(new ClassA);
二十二、类的继承:private,protected,public
以下内容整理自《程序员面试宝典》,重点讨论一下C++中三种继承方式的区别。
1.公有继承(public):
基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,protected和private成员不可见。
基类成员对派生类的可见性对派生类来说,基类的public和protected成员可见:基类的public成员和protected成员作为派生类的成员时,它们都保持原有状态;基类的private成员依旧是private,派生类不可访问基类中的private成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的public成员是可见的,其他成员是不可见的。
所以,在公有继承时,派生类的对象可以访问基类中的public成员,派生类的成员方法可以访问基类中的public成员和protected成员。
2.私有继承(private)
基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的public成员和protected成员是可见的:基类的public成员和protected成员都作为派生类的private成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的private成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以在私有继承时,基类的成员只能由直接派生类访问,无法再往下继承。
3.保护继承(protected)
保护继承与私有继承相似,基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见。
基类成员对派生类的可见性,对派生类来说,基类的public和protected成员是可见的:基类的public成员和protected成员都作为派生类的protected成员,并且不能被这个派生类的子类所访问;基类的private成员是不可见的:派生类不可访问基类中的private成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再向下继承。
C++支持多重继承。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类 是两个或多个基类的派生类时,派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们 。