基本概念:
引言:c是面向过程的,c++是面向对象,java是面向对象的,哪个效率更快一点,(面向对象的约定:保证所有数据成员为私有)
1、面向对象设计的三种特征及其意义。
三种特征是:
封装: 面向对象的设计基础,没有封装就没有对象,有封装就就引出一个类,类体现了封装性,一般的成员变量,都定义为私有的,成员函数为共有,这里有个类的成员变量,函数都定义为私有的,有什么作用呢?(访问受限)共有函数声明一个有缘类,就可以访问这个成员函数了。
继承 : 继承分为四种:一个类可以继承出另一个的特点,原基类,派生类,当派生类满足起需要的而定制基类的行为时,存在多态性关系,这里有一个问题了,一个类声明一个对象,那么这个对象的大小跟什么有关系呢?
函数
变量
排序:int a=5; int a=5;
Double b=1.0; char ‘c’;
Char ‘c’; double b=1.0;
虚拟继承 (不常用,)为什么它会存在,普通继承c++支持多种继承,所以就会出现一个虚拟继承。
共有继承
保护继承
私有继承
多态: 当派生类为满足其需要而定义基类的行为时存在则多态性关系,c++基类用虚函数成员函数来说明在派生类中重定义的该成员函数具有多态行为。
多态性是类层次结构中不同对象对同一个信息表现独特行为的能力。人事类层次结构中,成员函数formatteddisplay()正是表现出了多态行为。用基类类型引用的对象可假定具有众多的形态之一,最终的实现取决于该基类的派生类的特点以及是从哪一个派生类建立该实例化对象的。
2、 类设计原则。
1、 使接口小而全。
2、 封装性要好,尽量把成员声明为私有。
3、 容错性。
4、 智能性。
3、 内联函数与宏的意义及区别?
意义:把函数指定为内联,编译器会在每一个调用该函数的地方展开一个函数的拷贝,,简捷执行速度快。
内联函数:在函数调用处插入执行代码,消除函数调用开销。
宏:简单字符替换。
最常见的用法(定义了一个代表某个植的全局符号)(定义带参数的宏,可调用)
区别:
a) 处理阶段不同。宏是在预编译阶段处理,内联函数在编译阶段处理。
b) 宏不做数据类型检查,内联函数做数据类型检查。(作为一种约定,习惯上总是用大写字母来定义宏,宏还可以替代字符常量)
内联函数的用法:在函数定义处声明 inline 关键字,例子如下:
inline int CA::fun(void) {…}//注意:要在头文件写函数定义而不能在cpp文件里写函数定 义,因为需要使用此函数的文件一般只会包含头文件而不会包含cpp文件。
也可以直接在类定义里直接写函数体而不用inline 关键字,例子如下:
class CA{
public:
char fun(void) {…};
};
注意:不得在函数声明处加inline 关键字:
class CA{
public :
inline char fun(void); //错误,在函数声明处加inline 关键字无意义。
};
一般经常被调用且函数体很小的的函数声明为内联函数。内联函数会导致代码膨胀。
4、 构造函数、析构函数、拷贝构造函数、赋值操作符的作用。
构造函数(不可能是虚函数)用于对象初始化。调用构造函数时先分配空间然后做初始化的工作。构造函数不能有返回值,而且不能是void类型。可以重载,根据不同参数调用相应的构造函数。
析构函数(可以是许函数)在销毁对象时调用,比如delete 、 局部对象退出作用域或全局对象在程序退出时自动调用。析构函数不能有返回值和参数,而且不能是void类型。
拷贝构造函数用于从已存在的对象初始化一个新对象,例子:CA obj1(obj); 或
CA obj1= obj; //两种写法效果一样,但前一种写法更直观易懂。
赋值操作符在把一个对象的值赋给另一个对象时调用。
例子:CA obj1;
obj1= obj; //调用赋值操作符。
这里有个深拷贝和浅拷贝的问题
派生类中可以调用基类的构造函数吗?
当然可以 基类如无缺省构造函数则在派生类的构造函数中必须显示调用基类的构造函数。
析构函数都不能继承吗?
基类析构函数会在派生类析构是自动调用,
基类的构造函数在派生类中可以被调用,要是不调用也会自动调用,是必须的。
基类的析构函数则不能不派生类调用,但是会自动调用。
所以就是:
构造函数 会自动调用 可以自己调用
析构函数 会自动调用 自允许系统调用
5、虚函数与纯虚函数的用法及意义。
虚函数与纯虚函数体现多态性,是面向对象设计中非常重要的概念。多态也叫动态联编或晚绑定。如果基类与子类有相同(函数名与参数均相同)的虚函数,虚函数根据对象的类型调用,而非虚函数根据变量定义的类型调用。多态的底层机制是虚函数表。每个对象有个指向虚函数表的指针,调用时刻根据此指针查找虚函数表中匹配的函数。非虚函数在编译阶段
已决定了应该调用的函数。
注意:虚函数的调用机制只对指针和引用有效。
举例:
class CA{
public:
void f1(void) {cout<<”CA::f1”<<endl; }
virtual void f2(void) {cout<<”CA::f2”<<endl; }
};
class CB: CA{
public:
void f1(void) {cout<<”CB::f1”<<endl; }
virtual void f2(void) {cout<<”CB::f2”<<endl; }
};
void main(void)
{
CA obj1;
CA* pobj1 = NULL;//为空
CB obj2;
CB* pobj2 = new CB();//子类赋给父类
obj1 = obj2;
obj1.f1(); //输出 CA::f1,因为f1函数为非虚函数。
obj1.f2(); //输出 CA::f2,因为obj2是对象变量而非对象的指针或引用变量。
pobj1 = pobj2;//父类指针指到子类
pobj1->f1(); //输出 CA::f1,因为f1函数为非虚函数。
pobj1->f2(); //输出 CB::f2,体现动态调用特征。
CA& obj3 = obj2;
obj3.f2(); //输出 CB::f2,体现动态调用特征。
delete pobj2;
}
写出程序执行结果:
class People
{
private:
char name[25];
public:
People(char *ptr){strcpy(name,ptr);}
void hello()
{
cout<<"People:"<<name<<" hello!"<<endl;
}
};
class Teacher:public People
{
private:
char name[25];
public:
Teacher(char *ptr):People("Teacher"){strcpy(name,ptr);}
void hello()
{
cout<<"Teacher:"<<name<<" hello!"<<endl;
}
};
class Student:public People
{
private:
char name[25];
public:
Student(char *ptr):People("Student"){strcpy(name,ptr);}
void hello()
{
cout<<"Student:"<<name<<" hello!"<<endl;
}
};
void main()
{
People a("Cheshibu"), *p;
Teacher wang("wang");
Student li("li");
p=&a;
p->hello();
p=&wang;
p->hello();
p=&li;
p->hello();
}
输出结果是:::
People name hello
People teacher hello
People student hello
如果要是虚函数的话,结果是什么呢?
纯虚函数:也是一种虚函数,一般只给出声明而没有实现,留到派生类去实现。格式:virtual void fun(void)=0;纯虚函数用来定义接口,有纯虚函数的类称为抽象类。如果派生类仍没实现基类所有的虚函数,则派生类仍为抽象类。抽象类不能实例化,但可以定义抽象类的指针变量。例子:
CA obj; //错误,不能实例化抽象类。
CA* pobj= new CA(); //错误,不能实例化抽象类。
CA* pobj//正确
全部是纯虚函数的类称为接口或协议类。
6、类的静态成员函数与普通成员函数的区别。
a) 不能是virtual或const 函数。
b) 不能访问非静态成员。普通成员函数的参数表隐藏了最后一参数——this指针,静态成员函数没有this指针所以不能关联具体对象,它只与类相关。
c) 没有被派生类继承。
d) 既可以以对象调用也可以根据类来调用。
静态成员函数与全局函数类似,但把它封装在类的内部更好的体现了封装性。
7、 模板的意义和用法。
模板用于行为完全一致但数据类型不同的情况。
模板分两种:类模板和函数模板。类模板的格式:template <class T > class CA{…};
函数模板的格式:template <class T > T fun(…);
实例化类模板:CA<int> obj;
8、 友元的用法、意义和弊端。
友元分为友元类和友元函数。
友元类:如果一个类B是另一个类A的友员类,则B可以访问A的保护和私有成员。
友元函数:如果一个函数B是另一个类A的友员函数,则B可以访问A的保护和私有成员。友员函数可以是其它类的函数也可以是全局函数。例子:
class CA{
friend class CB; //CB是CA的友员类
friend int CC::Add(int); // CC::Add是CA的友员函数。
friend void g(int); // 全局函数是g是CA的友员函数。
};
友元是两个类之间的协议,即如果B是A的友员,B不能访问A的基类或派生类私有成员,B的派生类不能访问A的私有成员。友元是单向协议,即如果B是A的友员,则B可以访问A的成员,但A不能访问B的成员。
9、 虚拟继承的意义。
虚拟继承是为了解决多基派生所产生的二义性问题。如:A派生B、C,B、C派生D,
B和C都继承了A的成员,则D从B、C继承了两份。如果B、C是从A虚拟继承的,则可以消除二义性问题。例子:
class B: virtual A{…};
class C: virtual A{…};
虚拟继承会产生4个字节开销。
10、public、protected、private继承方式的区别?
public继承后父类的Public成员仍然是public的, protected成员仍然是protected的。
protected继承后父类的Public和protected成员都是protected的。
private继承后父类的Public和protected成员都是private的。
如果不修饰继承类型,缺省为public继承。
10引用的理解。引用与指针的区别?
C++中的引用是其他变量的别名,声明一个引用型变量,需要给它一个初始化值,在变量的生存周期内该值不可改变。
引用的三种用法:
1、定义引用类型的变量。
2、用引用传参。
3、函数返回类型为引用类型。
引用与指针的区别:
1、引用被创建的同时必须被初始化,指针可以在其它时候被初始化。
2、不能有NULL引用,指针则可以是NULL。
3、一旦引用被初始化,就不能改变,指针可以随时改变所指的地址。
12、怎样让类只生成唯一实例?
class Singleton
{
public :
static Singleton* Instance ( );
protected :
singleton ( ) ; //不会默认,首先写了一个构造函数,不会被私有化。
private :
static Singleton* _instance ;
};
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance ( )
{
if(_instance == 0)
{
_instance = new Singleton;
}
return _instance;
}
13、 重载、覆盖与隐藏三者的区别?
重载的条件:
a) 相同的范围(在同一个类中)。
b) 参数不同。
覆盖的条件:
a) 不同的范围(分别位于派生类与基类)。
b) 基类函数必须有virtual关键字。
c) 参数相同。
隐藏的条件:
a) 如果不是virtual函数,不论参数是否相同都是隐藏。
b) 如果是virtual函数,参数不同。
隐藏应极力避免,因为隐藏规则很晦涩,容易产生迷惑。
14、 构造函数与析构函数的调用次序,它们可不可以是虚函数,为什么?
构造函数的调用由外到里调用。例如类CA派生CB,在构造CB时先调用CA的构造函数然后调用CB的构造函数,析构正好相反,即先调用CB的析构函数再调用CA的析构函数。
构造函数不能为虚函数,因为虚函数调用以前对象空间还没有初始化,所以不存在虚函数表的指针,而调用虚函数须以虚函数表的指针查找虚函数表来调用相应函数。
析构函数可以并且一般都应为虚函数。当析构函数不是虚函数,如果父类指针对象指向子类对象,然后析构此指针对象时只调用了父类析构函数而子类没调用析构函数,导致析构不完全。
15、 怎样防止产生野指针?
野指针是指向垃圾内存的指针。产生野指针主要有三个地方:
a) 指针变量没有被初始化。
b) 指针被delete或者free之后,没有置为NULL。
c) 数组越界:超出数组最大范围比如是数组只有10,而指向了11
野指针是与内存泄漏相反的概念,野指针指内存不可用(未初始化或已回收),而指针 变量还可以使用。内存泄漏指变量不可使用(退出作用域或程序退出)而内存已被分配却没被释放。
16、 malloc、free、new、delete的关系和区别。
malloc、free是C里分配和释放堆内存的库函数。new、delete是C++分配和释放堆内存的操作符。不要混用(malloc分配的内存要用free释放,new分配的内存要用delete释放)
在C++里尽量使用new和delete,因为new会调用构造函数,delete会调用析构函数,而malloc和free不会做这个动作。
17、 C++内存分配方式有哪几种?
a) 从静态存储区域分配。内存在程序初始化的时候分配,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
b) 在栈上创建。在执行函数时,函数内局部变量的内存都可以在栈上创建,函数执行结束时自动被释放。栈分配效率很高,但是分配的内存容量有限。
c) 从堆上分配。程序在运行的时候用malloc或new申请任意多少的内存, 用free或delete释放内存。动态内存的生存期由程序员控制。
18、 C++共有几种类型转换方式?用途?
C++共有四种类型转换:
1、 静态转换。格式:static_cast<数据类型>(被转换变量)。作用:内部变量或无继承关系的类转换,相当于C的强制转换,不同的是static_cast在编译时做转换校验,如果转换无效会报错,另外,static_cast不作常量转换。
2、 动态转换。格式:dynamic_cast<数据类型>(被转换变量)。作用:父类向子类或兄弟类转换。
3、 常量转换。格式:const_cast<数据类型>(被转换常量)。作用:去掉常量属性,例如: const CA obj:;
// fun函数原型为fun(CA a);
fun(obj); //错误,常量不可传给变量。
fun(const_cast<CA>(obj)); //正确。去掉了obj的常量属性。
4、 执行期定义转换。格式:reinterpret_cast<数据类型>(被转换变量)。作用:在函数指针类型之间进行转换。注意:使用reinterpret_casts的代码很难移植。
19、 初始化参数表的格式,它与在构造函数里初始化有什么不同?
class CA::CA(int x):m_x(x), m_y(0), m_z(5) {…}
a) 在其初始化表里调用基类的构造函数,不能在构造函数里调用基类的构造函数。
b) 类的const常量只能在初始化表里初始化,不能在函数内用赋值的方式来初始化。
c) 对于对象类型成员变量,在初始化表里初始化有更高效率,因为在初始化表里初始化时直接调用此对象类的拷贝构造函数,而构造函数里初始化会调用此对象类的无参构造函数和赋值操作符函数。
20、 编译器会为类默认生成函数吗?如果有,是哪几个?
有构造函数、析构函数、拷贝构造函数、赋值操作符共四个函数。
21、 写一个操作符重载的例子。
CStr& CStr::operator=(const char* str)
{
delete [] pData; //假设Data为Cstr类的char*型成员变量
pData=new char[strlen(str)+1];
strcpy(pData, str);
return *this;
}
22、 简述继承与组合的意义。
公有继承体现is-a关系,例如哺乳动物CB是动物类CA的一种,CB具有CA的所有行为。私有继承体现‘用什么来实现’的关系,派生类具有某些基类特征。
组合体现派生类是基类的一部分,比如:脸类里包含有眼睛类实例。组合的类之间不存在继承关系,而是在一个类包含另一个类的实例从而使用另一个类的功能。
继承会产生类与类的偶合关系。
23、 缺省参数的声明与调用。
1、 缺省参数要放在参数表的最后面,从第一个缺省参数开始的后面所有参数必须都是
缺省的。
2、 缺省参数要在函数的声明而非定义中声明。
3、 缺省参数函数的的调用:后面没用缺省参数则它之前的所有参数也不能用缺省参数。即不能用空格代替缺省参数。
4、 缺省参数会产生二义性问题。例如:
int fun(int x, int y, char a=’x’ );
int fun(int x, int y, int z=0);
…
fun(2, 5); //会调用哪个函数呢?
24、 异常处理的格式和意义。
try //把有可能发生异常的括起来。
{
throw 5; // throw 后面跟异常类型。
}
catch(int e) //括号里为异常类型。如果抛出的异常为括号里的异常类型就执行此块语句。
{ … }
catch(CA e)
{ … }
catch(…) //如果抛出的异常与上面的异常类型都不同,进入缺省处理。
{ … }
void fun(…) throws a, b //限制此函数只抛出a、b两种异常。
25、++i和i++的区别?
1、++i是先把i加1然后返回,i++是返回后再加1。最好使用++i,对于i++会产生临时对象,这样降低了效率。
习题:
指出 const char *p, char const *p, 和 char * const p的区别
前两种一样,const修饰 char,即内容不可改变而地址可以改变。
char * const修饰 *,即内容可以改变而地址不能改变。
下面代码中是否存在错误?为什么?
A、
class CC{
int * m_pCount;
public:
void clear() {
if(m_pCount) delete m_pCount; //在下面加m_pCount = NULL;
}
CC() { m_pCount = new int; }
~CC() { clear(); }
};
B、
char * f1()
{
char * str1; //改为char * str1=new char[6],因为还未申请空间。
memcpy(str1,”hello”,6);
return str1;
}
C、
class A{
public:
static void f(A a);
private:
int x;
};
void A::f(A a) {
cout<<x; //错误,在静态函数里不能访问非静态成员。
cout<<a.x; //错误, 对象的私有成员不能访问。
}
读下面的程序,说出在屏幕上显示的结果是什么,并指出该程序运行后会有什么问题?
main()
{
char * p = new char[100];
strcpy(p, “hello”);
sub_func(p);
printf(“%s /n”, p);
delete p; //内存泄漏
}
void sub_func(char *p)
{
p = new char[100]; //内存泄漏
strcat(p, “weill”);
}
输出“hello”,因为指针也同样是值传递,形参的p new 之后不会影响实参本身,实参p仍然指向原来的地址。会出现的问题是:内存泄漏。
请写一个链表节点的增加函数
struct Node{
Node* pNext;
char* pszData;
};
//从一个单向链表中插入一个节点到链表的尾部
//pHead,链表头节点,已知链表头节点中不带有数据
//pszData,要插入的链表节点数据
//错误返回0,正确返回1
int InsertToLast(char* pszData)
{
if(pszData == null)
return 0;
Node* pTemp = pHead;
for(;pTemp->pNext != null;)
{ pTemp = pTemp->pNext;}
Node* pLast = new Node;
char* pData = new char[strlen(pszData)+1];
strcpy(pData , pszData)
pTemp->pNext = pLast;
pLast->pszData = pData;
pLast->pNext = null;
return 1;
}
写出下列程序执行结果
class X
{
public:
X(){cout<<”X0”<<endl;}
~X(){cout<<”~X0”<<endl;}
X(cont X &x){cout<<”X(x)”<<endl;}
};
void main()
{
X x1;
X x2(x1);
}
执行结果为:
X0
X(x)
~X0
~X0