面向对象总结
1 structure和class的区别?
structure和class的唯一区别就是默认的访问控制不同,structure默认是public,class默认是Private;structure也可以有构造函数、析构函数、成员函数等。
2 继承体系中为什么将析构函数声明为虚函数?
当你可能通过基类指针删除派生类对象时,建议使用虚析构函数。这样保证在删除一个执行派生类的基类指针时,不会出现只删除了基类部分,而没有删除派生类部分而导致内存泄露。
3 析构函数可以为virtual型,构造函数则不能。那么为什么构造函数不能为虚呢?
1)从存储的角度看,虚函数对应一个执行vtable虚函数表的指针,都知道,这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
2)虚函数采用一种虚调用的办法。徐调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准备对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。
3 为什么不把每个函数都声明为虚函数?
这是因为虚函数是有代价的:由于每个虚函数的对象必须维护一个指向虚函数表的指针,因此在使用虚函数的时候会产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么根本没有必要使用虚函数。
4 什么是多态?
多态性可以简单地概况成一句话“一个接口,多个方法“,在程序运行过程中才决定调用的函数。多态性是面向对象编程领域的核心概念。多态是通过虚函数实现的。
虚函数就是允许被其子类重新定义的成员函数。而子类重新定义父类虚函数的做法,称为”覆盖“,或者称为”重写“。覆盖是指子类重新定义父类的虚函数的做法。而重组,是指允许存在多个同名函数,而这些函数的参数表不同(或者参数个数不同,或者参数类型不同,或者两者都不同)。其实,重载的概念并不属于”面向对象编程“。重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对编译器来说是这样的)。它们的地址在编译期就绑定了,是静态的,因此,重载与多态无关。真正与多态相关的是”覆盖“。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数调用在编译期间是如法确定的。因此,函数是在运行期绑定的。结论是:重载只是一种语言特性,与多态无关,与面向对象也无关!
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类),它们的目的都是为了代码重用。而多态则是为了实现另一个目的——接口重用!
5 重载和覆盖有什么不同?
虚函数总是在派生类中被改写,这种改写被称为”override“(覆盖)。
override是指派生类重写基类的虚函数。重写的函数必须有一致的参数表和返回值。
overload是”重载“,是指编写一个与已有函数同名但是参数表不同的函数。重载不是一种面向对象的编程,而只是一种语法规则,重载与多态没有什么直接关系。
类的this指针 总结
类的this指针有以下特点:
(1)this只能在成员函数中使用
全局函数,静态函数都不能使用this。
实际上,成员函数默认第一个参数为T* const this。
如:
class A { public: int func(int p) { } };
其中,func的原型在编译器看来应该是:
int func(A* const this,int p);
(2)由此可见,this在成员函数的开始前构造,在成员的结束后清除。
这个生命周期同任何一个函数的参数是一样的,没有任何区别。
当调用一个类的成员函数使时,编译器将类的指针作为函数的this参数传递进去。如:
A a;
a.func(10);
此处,编译器将会编译成:
A::func(&a,10);
看起来和静态函数没有差别,对吗?不对,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高。
(3)几个this指针的易混淆问题。
1)this指针是什么时候创建的?
this在成员函数的开始执行前构造,在成员的执行结束后清除。
2)this指针存放在何处?堆、栈、全局变量,还是其他?
this指针会因编译器不同而又不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。
3)this指针是如何传递给类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么this指针又是如何找到“类实例后的函数”的?
大多数编译器通过eax寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。
在call之前,编译器会把对应的对象地址放到eax中。this是通过函数参数的首参数来传递的。
4)我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?
this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
6)每个类编译后,是否创建一个类中函数表保存函数指针以便用来调用函数?
普通函的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。
但是,即使是虚函数,如果编译器能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。
sizeof求字节以及与strlen的区别
例子一:
/*
*根据以下条件进行计算:
*1、 结构体的大小等于结构体内最大成员大小的整数倍
*2、 结构体内的成员的首地址相对于结构体首地址的偏移量是其类型大小的整数倍,比如说double型成员相对于结构体的首地址的地址
*偏移量应该是8的倍数。
*/
#include<iostream>
#include<cstdlib>
using namespace std;
class AA
{
int a;
short b;
int c;
char d;
};
class BB
{
double a;
short b;
int c;
char d;
};
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
int main()
{
cout<<sizeof(AA)<<" "<<sizeof(BB)<<endl;
char *ss1="0123456789";
char ss2[]="0123456790";
char ss3[100]="0123456789";
int ss4[100];
char q1[]="abc";
char q2[]="a\n";
char *q3="a\n";
char *str1=(char*)malloc(100);
void *str2=(void*)malloc(100);
cout<<sizeof(ss1)<<endl;//ss1是一个字符指针,指针的大小是一个定值,就是4
cout<<sizeof(ss2)<<endl; //ss2是一个字符数组,这个数组最初未定大小,由具体情况填充
cout<<sizeof(ss3)<<endl;//ss3也是一个字符数组,这个数组开始预分配100,所以它的大小一共是100位
cout<<sizeof(ss4)<<endl;//ss4也是一个整型数组,这个数组开始预分配100,但每个整型变量所占空间是4,所以它的大小一共是400
cout<<sizeof(q1)<<endl; //q1和ss2类似,所以是4
cout<<sizeof(q2)<<endl;//q2里面有一个'\n',算作一位,所以它的空间是3
cout<<sizeof(q3)<<endl;//q3是一个字符指针,指针的大小是一个定值,就是4,所以sizeof(q3)是4
cout<<sizeof(A)<<endl; //结构体的长度一定是最长的数据元素的整数倍
cout<<sizeof(B)<<endl;
cout<<sizeof(str1)<<endl; //指针的长度固定是4
cout<<sizeof(str2)<<endl;
}
其中,AA中,a占4个字节,b本应占2个字节,但由于c占4个字节,为了满足条件2,b多占用2个字节,为了满足条件1,d占用4个字节,一共16个字节。
BB中,a占8个字节,b占2个字节,但由于c占4个字节,为了满足条件2,b多占用2个字节,
即abc共占用8+4+4=16个字节,
为了满足条件1,d将占用8个字节,一共24个字节。
例子二:
#include<iostream>
#include<new>
using namespace std;
class A{};
class A2{
char d,e;
};
class B{
};
struct C{
char b,c;
};
struct D{
int x,y;
};
int main()
{
cout<<sizeof(A)<<endl; //对于一个类而言,即便它是一个空的类,编译器仍然要给它一个空间,所以类A即便什么都没有,它的空间大小也是1
cout<<sizeof(A2)<<endl; //而类A2大小是类中的两个字符d、e之和,所以它的空间大小依然是2
A *p1=new A();
A p2;
A *p3;
//至于p1 p2 p3,p1和p3是指针,所以它们的大小是一定的,因此是4,p2是类A的对象,所以它的大小和类A相等,也是1
cout<<sizeof(p1)<<endl;
cout<<sizeof(p2)<<endl;
cout<<sizeof(p3)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
cout<<sizeof(D)<<endl;
}
例子三:
总结:空的类也是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。
1)类内部的成员变量:
普遍的变量:是要占用内存的,但是要注意内存对齐(这点和struct类型很相似)
static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区
2)类内部的成员函数
非虚函数(构造函数、static函数、成员函数等):不占用内存
虚函数:要占用4个字节(32操作系统),用来指定虚拟函数表的入口地址。跟虚函数的个数没有关系。父类和子类共享一个虚函数指针。
构成对象本身的只是数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。成员函数为该类的对象共享,不仅是处于简化语言实现、节省存储的目的,而且是为了使同类对象由一致的行为。同类对象的行为虽然一致,但是操作不同的数据成员。
#include<iostream>
using namespace std;
class A1
{
public:
int a;
static int b; //static在全局变量区,不属于对象
A1();
~A1();
};
class A2
{
public:
int a;
char c;
A2();
~A2();
};
class A3
{
public:
float a;
char c;
A3();
~A3();
};
class A4
{
public:
float a;
int b;
char c;
A4();
~A4();
};
class A5
{
public:
double d;
float a;
int b;
char c;
A5();
~A5();
};
int main()
{
cout<<sizeof(A1)<<endl;
cout<<sizeof(A2)<<endl;
cout<<sizeof(A3)<<endl;
cout<<sizeof(A4)<<endl;
cout<<sizeof(A5)<<endl;
}
存在父类和子类以及虚继承时,sizeof的大小
总结:
- 类的大小为类的非静态数据成员的类型大小之和,静态成员数据不作考虑
- 空类的大小为1,单一继承和多重继承的空类空间为1,虚继承为4
- 构造函数、析构函数以及普通的成员函数跟sizeof无关
- 带有虚函数的类,因为要维护一个虚函数表所以占用一个指针的空间4
- 子类如果是重新实现的父类的虚函数,不计入sizeof大小
- 虚继承涉及到虚表(虚指针),大小为4
例如:
#include<iostream>
#include<memory>
#include<assert.h>
using namespace std;
class A{ };
class A2{};
class B:public A{};
class C:public virtual B{};
class D:public A,public A2{};
int main()
{
cout<<"sizeof(A): "<<sizeof(A)<<endl;
cout<<"sizeof(B): "<<sizeof(B)<<endl;
cout<<"sizeof(C): "<<sizeof(C)<<endl;
cout<<"sizeof(D): "<<sizeof(D)<<endl;
}
输出:
sizeof与strlen之间的区别:
例子一:
char *ss="0123456789";
sizeof(ss)结果为4,ss是指向字符串常量的字符数组。
sizeof(*ss)结果是1,*ss是第一个字符
例子二:
char ss[]="0123456789";
sizeof(ss)结果为11,ss是数组,计算到'\0'位置,因此是(10+1)。
sizeof(*ss)结果是1,*ss是第一个字符
例子三:
char ss[100]="0123456789";
sizeof(ss)结果是100,ss表示在内存中预分配的大小,100*1。
strlen(ss)结果是10,它的内部实现是用一个循环计算字符串的长度,直到'\0'为止,并且不包含'\0'。
例子四:
int ss[100]="0123456789";
sizeof(ss)结果是400,ss表示在内存中的大小,100*4。
strlen(ss)错误,strlen的参数只能是char*,且必须是以'\0'结尾的。
例子五:
class X
{
int i;
int j;
char k;
};
X x;
cout<<sizeof(X)<<endl; 结果是12,内存补齐
cout<<sizeof(x)<<endl; 结果是12,理由同上
通过上面的sizeof与strlen的深入理解,得出两者区别如下:
1)sizeof操作符的结构类型是size_t,它在头文件中的typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。
2)sizeof是操作符,strlen是函数
3)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以'\0'结尾的。sizeof还可以用函数做参数,比如:
short f();
printf("%d\n",sizeof(f()));
输出的结果是sizeof(short),即2
4)数组做sizeof的参数不退化,传递给strlen就退化为指针。
5)大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因:
char str[20]="0123456789";
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;
6)strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型占内存的大小。
7)sizeof后如果是类型必须加括号,如果是变量名可以不加括号。这是因为sizeof是个操作符而不是函数。
8)当使用一个结构类型或变量时,sizeof返回时实际的大小。当使用静态的数组空间时,sizeof返回全部数组的尺寸。sizeof操作符不能返回被动态分配的数组或外部的数组的尺寸。
9)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如:fun(char [8]),fun(char[])都等价于fun(char*)。在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。如果想在函数内知道数组的大小,需要这样做:进入函数后用memcpy将数组拷贝出来,长度由另一个形参传进去。代码如下:
fun (unsigned char *p1,int len)
{
unsigned char *buf=new unsigned char[len+1];
memcpy(buf,p1,len);
}
10)计算结构变量的大小就必须讨论数据对齐问题。为了使CPU存取的速度最快,C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐。
11)sizeof操作符不能用于函数类型、不完全类型或位字段。不完全类型指具有未知存储大小数据的数据类型,如未存储大小的数组类型、未知内容的结构或联合类型、void类型等。
sizeof对于指针,求出的是指针的大小,如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。
例如:
char var[10];
int test(char var[])
{
return sizeof(var);
};