疑问以及解答
0 前言
以下都是在学习c++时,遇到的疑问,解答也是通过查资料和自己的理解所得到的,不一定正确,有错误的地方,还望指正;会不定期的更新;
1 c++内存分布
c++程序的内存空间分局及对应的功能如下:
名称 | 功能 |
---|---|
全局数据区 | 存放全局变量、静态变量以及常量 |
栈区 | 调用函数时定义的局部变量、函数参数、返回数据、返回地址等 |
堆区 | 通过new 或者 malloc动态分配的地址空间 |
代码段 | 存放成员函数、非成员函数 |
2 c++内存对齐及对象的大小计算
内存对齐对程序员来说是透明的,一般情况下也用不上这方面的知识。内存对齐的原因:
1、 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:经过内存对齐后,CPU的内存访问速度大大提升,下面通过例子解释。
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。
假设CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:
1、数据从0字节开始
2、数据从1字节开始
再次假设内存读取粒度为4。
当该数据是从0字节开始时,很CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。
当该数据是从1字节开始时,问题变的有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。此时CPU先访问一次内存,读取0—3字节的数据进寄存器,并再次读取4—5字节的数据进寄存器,接着把0字节和6,7,8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。
这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。
先思考下如下两个个类所占的内存大小
#Include<iostream>
using namespace std;
class Base{
};
class A{
public:
int a;
char b;
short c;
void fun(){
}
};
class B{
public:
char a;
int b;
short c;
};
class C{
public:
int a;
char b;
short c;
virtual fun(){
}
};
class D{
public:
char a;
uint32_t b[n]; //n大于等于0
}
class E{
public:
char a;
uint32_t * b = new uint32_t[n];//n大于等于0
};
int main(){
cout<<sizeof(Base)<<endl;
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
cout<<sizeof(D)<<endl;
cout<<sizeof(E)<<endl;
}
类或对象所占大小与内存对齐大小及规则息息相关,所以,有必要先给出内存对齐的规则:
1.数据成员对齐规则:struct, union的数据成员,第一个数据成员放在offset为0的地方,之后的数据成员的存储起始位置都是放在该数据成员大小的整数倍位置。如在32bit的机器上,指针的大小为4,因此int存储的位置都是4的整数倍的位置开始存储。
2.结构体作为数据成员的对齐规则:在一个struct中包含另一个struct,内部struct应该以它的最大数据成员大小的整数倍开始存储。如 struct A 中包含 struct B, struct B 中包含数据成员 char, int, short,则 struct B 应该以sizeof(int)=4的整数倍为起始地址。
3.收尾工作的对齐规则:整个struct的大小,应该为最大数据成员大小的整数倍。
还有一点需要注意的是,类的静态成员以及成员函数是不占用类或对象的空间,如果类中含有虚函数,则该类中会隐式的含有一个指向虚表的指针。 现在以相对于类的起始位置偏移来分析类的大小:
- 规定对于不含任何成员的类来说,其大小固定为1;
- 对于类A来说,成员a偏移为0,所占大小为4个字节,成员b偏移为4,占用1个字节,成员c的偏移为6,因为short类型的大小为2,而前面已经占用了5个字节,又偏移为其大小的整数倍,所以从偏移为6(假设从0字节出开始存储)的位置6开始存储。静态成员和函数不占空间,此时,类已经占用了8个字节的空间,符合内存对齐的第三条规则。
- 对于类B来说,成员a的偏移量为0,占用内存大小为1,对于成员b来说,其占用内存的大小为4,根据内存的对齐的规则1来说,其开始的位置从第4个字节开始开始,成员c开始的位置从第8个字节开始,此时类已经占用了10个字节,但根据内存对齐的规则3,还需要占用两个字节的空间,因此,类B所占空间大小为12.
- 对于类C来说,由于其含有虚函数,所有一开始会有一个虚表指针,在64位系统下,占用8个字节,32位系统下,就占用4个字节;这里以64位系统为例,那么成员a的偏移为8,成员b的偏移为12,成员c的偏移为14,short型占用大小为2个字节,总大小为16字节,也符合规则3。
- 类D和类E有的成员比较相似,前者含有一个数组,后者含有一个指向数组的指针。对于类D来说,数组的是存储在结构体中的,因此,在计算其大小时,要包括数组的大小,并且此时的对齐规则按照其所存储的类型计算,并且,数组的大小可以为0,此时,虽然数组不占用空间,但依然要遵循内存对齐规则,即其位置从偏移4开始,当n=0时,该类的大小即为4,如果n为其他正整数,其大小为(4+4*n)。对于类E来说,其中的指针指向动态分配的内存,类中只存储该指针的大小,并且内存对齐规则按照指针的大小计算,在64位系统下,其大小为8个字节,因此其存储位置从偏移8开始计算,占用8个字节,总的大小为16。
另外值得注意的是,c++中 通过#pragma pack(n)可以改变默认的对齐规则,如果指定的话,对齐规则会按照当前成员大小和n中的小值进行。
参考:百度百科
3 为什么类的静态成员不能声明成const?
当通过类的对象来调用成员函数时,实际上是通过一个名为this的额外隐式参数来访问该对象,this是一个指向该对象的指针。this形参是隐式定义的,实际上,任何自定义名为this的参数或变量的行为都是非法的,但我们可以在成员函数体内部使用this。默认情况下,this的类型是指向类类型非常量版本的常量指针,
class Base{
public:
int fun(){
return this->a;//等价于return a;
}
int a;
};
int main(){
Base c;
c.fun();//当调用该函数时,this指针被初始化为c的地址;
const Base c1;
c1.fun();//该调用是非法的;
return 0;
}
//this的默认类型是Base * const;此时,其不能指向类型为Base的常量对象;
由上定义可知,this是一个顶层const对象,表明其自身是一个常量,但其所指的对象是个变量,那么就意味着默认情况下this指针不能指向常量版本的类类型对象;这一情况也就使得我们不能在常量对象上调用普通的成员函数(因为我们不能将常量对象的地址赋值给指向变量的指针)。为了使得常量对象能够调用类的成员函数,我们可以函数定义的形参列表后加上const,表示this是一个指向常量的指针,这样的函数称之为常量成员函数,通过区分成员函数是否是const的,我们可以实现成员函数的重载;
class Base{
public:
int fun(){
cout<<"I am an non-const member function"<<endl;
return this->a;//等价于return a;
}
int fun() const {
cout<<"I am an const member function"<<endl;
return this->a;//等价于return a;
}
int a;
};
int main(){
Base c;
c.fun();//调用非常版本的成员函数
const Base c1;
c1.fun();//调用常量版本的成员函数
return 0;
}
//非常量对象会优先调用非常量版本的成员函数,而常量对象只能调用常量版本的成员函数
类的静态成员成员是不与任何对象绑定的,虽然可以通过对象或者类名来进行调用,但其不包含this指针,而在成员函数后加const是用来修饰this指针的,那么静态成员函数就不能声明是const的,并且在静态成员函数的内部只能使用类的静态成员,任何在静态成员函数的内部显示或隐式的使用this指针都是无效的;
class A_ {
public:
A()=defalult;
A(int a_):a(a_) {}
void f() {
cout << "non-static fuction" << endl;;
}
void static f_1() {
cout << "call static function f_1" << endl;
cout << "static member variable: b=" << b << endl;
f_2();
//f();非法调用
//cout << a;//非法使用
}
void static f_2() {
cout << "call static function f_2"<<endl;
}
int a = 9;
static constexpr int b=9;
};
int main(){
A_ object;
object.f_1();//等价于 A_::f_1();
return 0;
}
输出如下:
4 类的静态函数为什么不能定义成虚函数?
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable. 对于静态成员函数,它没有this指针,所以无法访问vptr. 所以static函数不能为virtual.
5 类的构造函数为什么不能定义成虚函数?
关于C++为什么不支持虚拟构造函数,Bjarne很早以前就在C++Style and Technique FAQ里面做过回答
Avirtual call is a mechanism to get work done given partialinformation. In particular, “virtual” allows us to call afunction knowing only an interfaces and not the exact type of theobject. To create an object you need complete information. Inparticular, you need to know the exact type of what you want tocreate. Consequently, a “call to a constructor” cannot bevirtual.
出处:Stroustrup: C++ Style and Technique FAQ
含义大概是这样的:虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数,再者,虚函数作用是为了完成多态性,而构造函数只在定义对象的时候使用,是一次性的行为,将其定义其虚函数也没意义。
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过父类的指针或引用来调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
来源:https://blog.youkuaiyun.com/shilikun841122/article/details/79012779
6 为什么不存在引用的数组?
引用的数组,即数组中的每个元素都是引用类型,由于在定义引用时必须初始化,而数组是不允许拷贝和赋值的,即不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值;这也就意味着在定义引用的数组时,没办法同时初始化其中的元素,所有也就不能定义引用的数组;在一个,数组中的元素要求是对象,而引用并不是对象,只是对象的别名;但我们可以定义数组的引用;
int & arr[4] //非法定义,将arr声明成引用的数组
int (&arr)[4] //合法定义,arr是具有4个整数的整形数组的引用
7 引用的数组、数组的引用、指针的数组以及数组的指针以及作为函数函数时的用法
引用的数组:非法,原因如上;
引用的数组:类似于普通变量的引用;
int (&arr)[4] //arr是具有4个整数的整形数组的引用,一定要加括号
当函数的形参为数组的引用时,维度也是类型的一部分,在传递实参时,维度必须匹配
void fun( int (&arr)[10]){
//do something
}
int main(){
int arr_1[5]={0};
int arr_2[10]={0};
fun(arr_1); //将会报错,维度不匹配
fun(arr_2); //合法调用
return 0;
}
指针的数组:即数组中的每个元素是是指针;
int * arr[10]; //arr是个数组,其中存放10个指向整形的指针
当指针数组作为函数形参时,传递给函数的实参的维度会被忽略,即实参可以为指针的数组或指针的指针
void fun(int *arr[10] ){
//do something
}
int main(){
int **p=nullptr;
int *arr[10];
int *arr_1[5];
fun(p); //合法调用
fun(arr) ; //合法调用
fun(arr_1); //合法调用
return 0;
}
数组的指针:即指针指向的是一个数组
int (*arr)[10]; //括号必不可少
当数组的指针作为形参时,维度必须匹配
voif fun(int (*arr)[5 ] ) {
//do something
}
int main(){
int arr_1[10];
int arr_2[5];
fun(&arr_1); //非法调用
fun(&arr_2); //合法调用
return 0;
}
8 宏定义和内联函数的区别
- 内联函数是一个函数,在运行时可以进行调试,而宏定义不是函数,不可以进行调试。
- 宏是由预处理器对宏进行替换的,而内联函数是通过编译器控制实现的。编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
- 在类的内部定义的函数是内联函数且可以访问类的成员变量,宏定义则不能。
9 顶层const和底层const的区别
顶层const:表明自身是个const对象,任何对象都可以是const;
底层const:表示所指向的对象为const, 通常与指针和引用等复合类型有关;
const int a =0; //顶层cosnt
int * const p = &a; //顶层
const int *p2=&a; //底层
const int& b = a; //底层
const int * const q = &a; //q所指的对象是个常量,同时他自身也是个常量,所以去q即使底层const又是顶层const
10 值初始化和默认初始化的区别
默认初始化:如果定义变量时,没有指定初始值,则变量被默认初始化,此时,变量被赋予了”默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响;
- 定义与任何函数体之外的变量被初始化为0或空;
- 当在块的作用域内不使用任何初始值定义一个非静态变量或数组时,此时值是未定义的,与分配的内存有关;
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时;
- 当类类型的成员没有在构造函数初始值列表中显示地初始化时;
值初始化:
- 在数组初始化的过程中,如果提供的初始值数量少于数组的大小时;
- 当不使用初始值定义一个局部静态变量时;
- 当通过书写如T()的表达式显示地接收请求初始值时,其中T是类型名(vector的一个构造函数只接受一个实参说明vector的大小),它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化。
11 new/delete和malloc/free的区别
以下转载自link,原博客中有更详细的说明。
12 全局变量、静态变量和类的静态成员
全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。静态变量和全局变量一样,没有显式赋值的话,值为0;但是有一点例外,就是在类中定义的静态变量,要么在定义时得进行初始化(只有常量可以这样做),要么在类外初始化;否则在使用时,会报未定义的引用错误。
#include <iostream>
using namespace std;
class Base {
public:
//static int a = 8; //非静态常量,不能在类内初始化,取消注释将会报错
static const int b = 9; //对于静态常量,可以在定义时,进行初始化操作
private:
static int c;
static int d;
};
int Base::c = 1; //只能在定义初始化时,才能访问私有静态变量
int main(){
Base base;
base.d; //访问私有静态变量,将会报错
}
全局静态变量只在定义的文件内可见,但全局变量的可见范围超出了文件域;
a.cpp
#include<iostream>
using namespace std;
int a = 0;
static int b = 1;
extern void fun();
int main(){
fun();
}
b.cpp
#include<iostream>
using namespace std;
extern int a; //必须要加
extrern int b;
void fun(){
cout<<a<<endl;
//cout<<b<<endl; //取消注释,将会报无法引用的外部错误
}
静态局部变量,只会初始化一次;
#include<iostream>
using namespace std;
void fun(int a){
static int d = a;
cout<<d<<" "<<a<<endl;
}
int main(){
for(int i = 0; i < 5; i++){
fun(i);
}
}
//输出如下
0 0
0 1
0 2
0 3
0 4
更详细的讲解可以参考:https://blog.youkuaiyun.com/u014186096/article/details/48391267