赞
踩
答:虚函数声明如下: virtual ReturnType FunctionName(Parameter);引入虚函数是为了动态绑定。
纯虚函数声明如下:virtual ReturnType FunctionName()= 0;引入纯虚函数是为了派生接口。
虚函数
定义一个函数为虚函数,不代表函数为不被实现的函数
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要再派生类中声明该方法为虚方法。
当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。
纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0;
引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
c++中包含纯虚函数的类是不允许被实例化的!!!,进一步说,如果继承该类的类不重写这个纯虚函数的话,也是不允许被实例化的。即,包含纯虚函是的类派生出来的类都必须重写这个纯虚函数!
为什么要有这个机制呢?
例如动物可以派生出猫、狗等。 猫和狗可以实例化,而动物这个概念是不可以实例化的。
1、静态成员函数; 2、类外的普通函数; 3、构造函数; 4、友元函数
虚函数是为了实现多态特性的。虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数,其是有有如下几点需要注意:
(1) 类的构造函数不能是虚函数
构造函数是为了构造对象的,所以在调用构造函数时候必然知道是哪个对象调用了构造函数,所以构造函数不能为虚函数。
(2) 类的静态成员函数不能是虚函数
类的静态成员函数是该类共用的,与该类的对象无关,静态函数里没有this指针,所以不能为虚函数。
(3)内联函数
内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。
(4)友元函数
友元函数与该类无关,没有this指针,所以不能为虚函数。
防止内存泄露,delete p(基类)的时候,它很机智的先执行了派生类的析构函数,然后执行了基类的析构函数。
如果基类的析构函数不是虚函数,在delete p(基类)时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。
答:通常在类外初始化static数据成员,但是 static const 的整型(bool,char,int,long)可以再类声明中初始化,static const的其他类型也必须在类外初始化(包括整型的数组)。
const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。
static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate = 2.25;static关键字只能用于类定义体内部的声明中,定义时不能标示为static
在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
const数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。
const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。
答:总的思想是RAII:设计一个class,令他的构造函数和析构函数分别获取和释放资源。
有两个方法:
利用“函数的局部对象无论函数以何种方式(包括因异常)结束都会被析构”这一特性,将“一定要释放的资源”放进局部对象的析构函数;
使用智能指针。
private继承与public的继承是完全不同的,主要体现在两个地方:
其一,public继承在子类中保持父类的访问权限,即父类中是public的成员函数或成员变量,在子类中仍是public,对private或者protected的成员函数或成员变量亦是如此;但private继承则不是这样了,它破坏了父类中的访问权限标定,将之都转成private,这对子类本身并无影响(照常访问),但却影响了子类的子类,子类的子类将无法访问这些声明/定义在爷爷辈类的成员变量或成员函数。
其二,Liskov法则不再适用,也就是说“一切父类出现的地方都可以被子类所替代”的法则在private这里不成立
答: (1)、构造函数抛异常:不会发生资源泄漏。假设在operator new()时抛出异常,那么将会因异常而结束此次调用,内存分配失败,不可能存在内存泄露。假设在别处(operator new() )执行之后抛出异常,此时析构函数调用,已构造的对象将得以正确释放,且自动调用operator delete()释放内存
析构函数抛异常:
可以抛出异常,但该异常必须留在析构函数;若析构函数因异常退出,情况会很糟糕(all kinds of bad things are likely to happen)
a、可能使得已分配的对象未能正常析构,造成内存泄露;
b、例如在对像数组的析构时,如果对象的析构函数抛出异常,释放代码将引发未定义行为。考虑一个对象数组的中间部分在析构时抛出异常,它无法传播,因为传播的话将使得后续部分不能正常释放;它也无法吸收,因为这违反了”异常中立“原则(异常中立,就是指任何底层的异常都会抛出到上层,也就相当于是异常透明的)。
(2)、抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部如果是,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。
答:使用mutable去掉const的成员函数的const性质
const_cast和mutable的比较
const_cast:
1) 强制去掉对象的const属性。
2) 缺点:对const对象,调用包含const_cast的const成员函数,属于未定义行为。
mutable:
1) 使用场景:对可能要发生变化的成员前,加上存储描述符mutable。
2) 实质:对加了mutable的成员,无视所有const声明。
为什么要有这种去除常量标志的需求?
答:两个概念:物理常量性和逻辑常量性
物理常量性:实际上就是常量。
逻辑常量性:对用户而言是常量,但在用户不能访问的细节上不是常量。
答:(1)、
a、使用单参数的构造函数或N个参数中有N-1个是默认参数的构造函数,如:
class A
{
public:
A(stirng s);
A(string s,int a = 0);
};
class String
{
public:
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
String s1 = “hello”; //OK 隐式转换,等价于String s1 = String(”hello”),将char型变成了string类型
b、使用operator what_you_want_to_convert_type() const
class A
{
public:
operator char*() const
{
return data;//当从其他类型转换到char*时自动调用
}
private:
char* data;
};
(2)、避免隐式类型转换的方法:在单参数的构造函数或N个参数中有N-1个是默认参数的构造函数声明之前加上explicit。
解答:这个问题主要是针对连续内存容器和非连续内存容器。
a、对于连续内存容器,如vector、deque等,增减元素均会使得当前之后的所有迭代器失效。因此,以删除元素为例:由于erase()总是指向被删除元素的下一个元素的有效迭代器,因此,可以利用该连续内存容器的成员erase()函数的返回值。常见的编程写法为:
for(auto iter = myvec.begin(); iter != myvec.end()) //另外注意这里用 "!=" 而非 "<"
{
if(delete iter)
iter = myvec.erase(iter);
else ++iter;
}
还有两种极端的情况是:
(1)、vector插入元素时位置过于靠前,导致需要后移的元素太多,因此vector增加元素建议使用push_back而非insert;
(2)、当增加元素后整个vector的大小超过了预设,这时会导致vector重新分分配内存,效率极低。因此习惯的编程方法为:在声明了一个vector后,立即调用reserve函数,令vector可以动态扩容。通常vector是按照之前大小的2倍来增长的。
b、对于非连续内存容器,如set、map等。增减元素只会使得当前迭代器无效。仍以删除元素为例,由于删除元素后,erase()返回的迭代器将是无效的迭代器。因此,需要在调用erase()之前,就使得迭代器指向删除元素的下一个元素。常见的编程写法为:
for(auto iter = myset.begin(); iter != myset.end()) //另外注意这里用 "!=" 而非 "<"
{
if(delete iter)
myset.erase(iter++); //使用一个后置自增就OK了
else ++iter;
}
其实在C++11中erase的返回值就是下一个节点,也可以利用函数的返回值。
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
1. 申请的内存所在位置不同,
mollloc函数从自由存储区(free store)上为对象动态分配内存空间,而new操作符从堆上动态分配内存。new甚至可以不为对象分配内存!定位new的功能可以办到这一点:new (place_address) type,operator new不分配任何的内存,它只是简单地返回指针实参。
2.返回类型安全性
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
3.内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
4.是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
5.是否调用构造函数/析构函数
new 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
第三步:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
第一步:调用对象的析构函数。
第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
6.对数组的处理
C++提供了new[]与delete[]来专门处理数组类型:。
7.是否相互调用
new底层其实还是用malloc实现动态内存分配的,所以new调用malloc,malloc不会调用new。
8.函数重载
new可以重载
malloc不行
java语言有自己的垃圾回收机制,而c/c++却要程序员手动的释放用关键字new或者 malloc系统函数申请的内存空间,然而由于程序员的疏忽可能会忘记去手动释放内存,这样就导致了程序内存的泄漏。
a、使用RAII(Resource Acquisition Is Initialization,资源获取即初始化)技法,以构造函数获取资源(内存),析构函数释放。
b、相比于使用原生指针,更建议使用智能指针,尤其是C++11标准化后的智能指针。
c、注意delete和delete[]的使用方法。
d、这是很复杂的一种情况,是关于类的copy constructor的。首先先介绍一些概念。
解答:STL中的sort(),在数据量大时,采用quicksort,分段递归排序;一旦分段后的数量小于某个门限值,改用Insertion sort,避免quicksort深度递归带来的过大的额外负担,如果递归层次过深,还会改用heapsort。
相同点:
●都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
不同点:
●指针是一个实体,而引用仅是个别名;
●引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
●引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
●引用不能为空,指针可以为空;
●“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
●指针和引用的自增(++)运算意义不一样;
●引用是类型安全的,而指针不是 (引用比指针多了类型检查)
指针数组
首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
char *arr[4] = {"hello", "world", "shannxi", "xian"};
数组指针
首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
char (*pa)[10];
https://blog.csdn.net/coolwriter/article/details/78614933
指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。
int *GetDate();
int * aaa(int,int);
函数指针是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); /*声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
#include <iostream>
using namespace std;
typedef int(*PF)(int, int);
//int(*func)(int a, int b);
int bar(int a, int b)
{
return a + b;
}
int foo(int a, int b)
{
return a;
}
int _tmain(int argc, _TCHAR* argv[])
{
PF func;
func = bar;
cout << func(12, 34) << endl;
system("pause");
func = foo;
cout << func(12, 34) << endl;
system("pause");
return 0;
}

一旦知道函数指针是如何工作的,我们就可以构建一些复杂的定义,例如:
void *(*(*fp1)(int))[10];
fp1是一个指向函数的指针,该函数接受一个整型参数,并且返回类型是一个指向包含了10个void指针数组的指针。是不是很绕?
float (*((*fp2)(int,int,float)))(int);
fp2是一个指向函数的指针,该函数接受三个参数(int,int,float),返回值是一个指向函数的指针,该函数接受一个整型参数并返回一个float。
typedef doubele (*(*(*fp3)())[10])();
fp3是一个函数指针,该函数无参数,且返回一个指向含有10个指向函数指针指针数组的指针,这些函数不接收参数,且返回值是double值
int (*(*fp4())[10])();
fp4是一个返回指针的函数,该指针指向含有10个函数指针的数组,这些函数的返回值是整型。
// a = new int*[n];//创建行指针数组
int **a = new int*[n];//这一步是前面两行的结合体
for(int i=0;i<n;++i)
{
*(a+i)= new int[n];//为每一行分配空间
//a[i] = new int[n];
memset(a[i],0,n*sizeof(int));
}
首先是如何申请二维的数组,这里我们先申请一个指针数组,然后令指针数组中的每一个元素都指向一个数组,这样二维数组就
size_t row, col;
//输入row和col的数值
int **MathTable = new int*[row];
for (int i = 0; i < row; i++)
MathTable[i] = new int[col];
然后是释放空间的过程:
//code
for (int i = 0; i < row; i++)
delete[] MathTable[i];
delete[]MathTable;
符合new和delete配对的原则,怎么new出来就怎么delete掉。
1、标准:分别隶属于两个不同的标准委员会。C以C99标准为主流,C11已经发布;C++以C++98/03为主流,C++11/14也日趋流行。
2、语言本身:
常用语言/库特性:
仅有C++才有的常用特性:
语言(范式)特性:
在类型方面,有运行时类型信息(RTTI)等技术作为C++类型技术的支撑。
常见关键字(操作符)特性:
常量定义:
常用新特性:
C/C++中的内存对齐之基础篇
这里主要以【不带有】虚继承链和虚函数的struct/class为例,【注意:所有的空间均需要为最大类型大小的整数倍】
这里有一个C和C++不同的情况:在C中,空struct/class不占有内存,在C++中,空struct/class通常占有1byte的空间,原因是编译器强行在里面放入了一个char,这样可以使得这个class的不同实例化在内存中分配到独一无二的地址。
最基本的内存对齐情况(其中注释代表该类型实际大小)
struct A
{
char c; //1byte
double d; //8byte
int i; //4byte
};
在64位g++和vs2013下运行sizeof(A)结果均为24。这种情况下的计算都比较简单,首先确定最大类型的大小,这里是double,因此Max = 8,因此占据的空间就应该是8的倍数(相应的,若struct内最大的类型为int,那么占据的空间就应该是4的倍数)。补齐的大小就根据最大类型的长度来确定。通常在内存中按照变量声明的顺序来分配空间,先为char分配,占据1byte, 8 - 1 = 7,剩余空间小于下一个变量double的需要空间,因此另外开辟一个8byte用于安放double,紧接着安放int,它占据另一个8byte空间的4个byte。而char后面的7byte、int后面的4byte都用于内存对齐。
因此总大小为8+8+8 = 24(可以看成1+7 + 8 + 4+4)。
struct A
{
double d; //8byte
char c; //1byte
int i; //4byte
};
在64位g++和vs2013下运行sizeof(A)结果均为16。根据上述说明,很容易得到 8 + 1+4+3 = 16,其中3为char、int之后的补齐。
稍复杂一点的内存对其情况
class A
{
public:
static double dd;
char c; //1byte
double d; //8byte
static A a;
int i; //4byte
};
在64位g++和vs2013下运行sizeof(A)结果均为24。这里只需要注意一下,static data member会被放在class object之外。因此sizeof(A)时,不会计算他们的大小在内。其余计算同 2 中的第一个例子相同。
只有一种类型时的情况:如一个struct中仅有一个char或int等,由于“所有的空间均需要为最大类型大小的整数倍”这个原则,struct的空间大小就是这些类型各自的大小,而不用再进行补齐。
C/C++中的内存对齐之深入篇——虚继承和虚函数
class A
{
public:
virtual ~A();
char c; //1byte
double d; //8byte
int i; //4byte
};
在32位g++下运行sizeof(A)结果为24,在64位g++下运行sizeof(A)结果为32,在vs2013下运行sizeof(A)结果为32。
32位g++下:通常将vptr放在object的最前面,可以确定该内存空间与data member的内存空间不需要独立。也就是说,该例中,无论虚析构函数被声明在哪里,都会在分配空间时最先给一个vptr分配4byte的空间,且该空间后是可以紧接着分配char的1byte的空间。以此类推,结合上面的例子,可以得出4(vptr)+1(char)+3(补齐) + 8 + 4+4 = 24
64位g++下:通常将vptr放在object的最前面,无法确定该内存空间与data member的内存空间是否需要独立。也就是说,该例中,无论虚析构函数被声明在哪里,都会在分配空间时最先给一个vptr分配8byte的空间,且不清楚该空间后是否可以紧接着分配char的1byte的空间(由于该vptr占据8byte,无论是否需要间隔,效果都一样),以此类推,结合上面的例子,可以得出
8(vptr)+ 1(char)+7(补齐) + 8 + 4+4 = 32
在vs2013下:通常将vptr放在object的最前面,vptr的大小与实际最大类型的大小相关。也就说说,该例中,无论虚析构函数被声明
在哪里,都会在分配空间时最先给一个vptr分配4byte的空间,由于后面存在double类型,需要将vptr补齐。结合上面的例子,可以得出
4(vptr)+4(补齐) + 1+7 + 8 +4+4 = 32
2、带有普通继承的class的内存对齐情况
class A
{
int i; //4byte
char c1;//1byte
};
class B : public A
{
char c2;//1byte
};
class C : public B
{
char c3;//1byte
};
在64位g++下,调用sizeof(A)、sizeof(B)、sizeof(C)后的结果均为8;在vs2013下分别为8,12,16
g++下:普通继承时,派生类和基类的内存空间没有间隔。
A:4+1+3(补齐) = 8
B:4+1+1(c2)+2(补齐) = 8
C:4+1+1(c2)+1(c3)+1(补齐) = 8
注意这里所有成员均为私有成员,如果改成public或protected则大小会有变化
vs2013下:普通继承时,派生类和基类的内存空间需要独立,即先补齐基类,再分配派生类。
A:4+1+3(补齐) = 8
B:4+1+3(补齐) + 1(c2)+3(补齐) = 12
C:4+1+3(补齐) + 1(c2)+3(补齐) + 1(c3)+3(补齐) = 16
3、带有虚拟继承链的class的内存对齐情况
class A
{
int i; //4byte
char c1;//1byte
};
class B : virtual public A
{
char c2;//1byte
};
class C : virtual public B
{
char c3;//1byte
};
调用sizeof(A)、sizeof(B)、sizeof(C)后,32位g++下,分别为8,16,24;64位g++下,分别为:8,24,40;vs2013下分别为8,16,24
32位g++下:
A:仍然是4+1+3(补齐) = 8
B:4+1+3 + 4(vptr)+1(c2)+3(补齐) = 16
C;4+1+3 + 4(vptr)+1(c2)+3(补齐) + 4(vptr)+1(c3)+3(补齐) = 24
64位g++下:
A:仍然是4+1+3(补齐) = 8
B:4+1+3 + 8(vptr)+1(c2)+7(补齐) = 24
C;4+1+3 + 8(vptr)+1(c2)+7(补齐) + 8(vptr)+1(c3)+7(补齐) = 40
vs2013下:
A:仍然是4+1+3(补齐) = 8
B:4+1+3 + 4(vptr)+1(c2)+3(补齐) = 16
C;4+1+3 + 4(vptr)+1(c2)+3(补齐) + 4(vptr)+1(c3)+3(补齐) = 24
注意这里vs2013的情况表面看上去和32位g++相同,实则不然。例如去掉class B对于A的虚拟继承性
class A
{
int i; //4byte
char c1;//1byte
};
class B : public A /*注意这里跟上面相比不是虚拟继承了*/
{
char c2;//1byte
};
class C : virtual public B
{
char c3;//1byte
};
调用sizeof(A)、sizeof(B)、sizeof(C)后,32位g++下:分别为8,8,16;vs2013下分别为8,12,20
32位g++下:
A:仍然是4+1+3(补齐) = 8
B:B:4+1+1(c2)+2(补齐) = 8(因为不是虚拟继承)
C;4+1+1(c2)+2(补齐) + 4(vptr)+1(c3)+3(补齐) = 16
vs2013下:
A:仍然是4+1+3(补齐) = 8
B:4+1+3(补齐) + 1(c2)+3(补齐) = 12
C;4+1+3(补齐) + 1(c2)+3(补齐) + 4(vptr)+1(c3)+3(补齐) = 20
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。