不要在构造函数和析构函数中调用虚函数
下面说说原理:
假如基类有个虚函数
那么编译器会为其创建虚函数表vtbl
并在对象的内存空间创建虚函数指针vptr
虚函数表的原理是每个类会在里面有自己的所能调用到的虚函数地址
对象的内存空间一般只有两样东西:
虚函数指针vptr 和 数据成员(包括直接基类和间接基类的)
在对象初始化过程中
先构造基类对象再构造派生类对象
也就是说当前正在执行基类的构造函数时
执行完初始化列表之后 执行构造函数体之前
编译器插入了初始化vptr的逻辑
令对象的vptr指向是基类的vtbl
所以此时无论如何也访问不到派生类的vtbl
也就无法调用派生类中覆盖(overrided)了的虚函数
只有执行到派生类的构造函数时
才能更新vptr指向派生类的vtbl
此后调用的虚函数才是派生类中覆盖了的虚函数
析构函数函数是构造函数的逆过程
所以在派生类的析构函数中的vptr
基类析构函数中的vptr是不同的
同样是执行析构函数体之前就被改为了对应当前类的vptr
下面我们做个实验验证一下
struct Base {
void printVPTR() {
void (***pvptr)() = reinterpret_cast<void (***)()>(this);
void (**vptr)() = *pvptr;
printf("%p\n%p\n", this, vptr);
}
Base() {printVPTR();}
virtual ~Base() {printVPTR();}
};
struct Derived : Base {
Derived() {printVPTR();}
~Derived() override {printVPTR();}
};
int main() {
Derived d;
}
打印结果:
0x7fff5fbff748
0x1000010a8
0x7fff5fbff748
0x100001060
0x7fff5fbff748
0x100001060
0x7fff5fbff748
0x1000010a8
各位可以看到构造/析构过程中this指针是没变的
因为基类子对象和派生类子对象的区域是重叠的
都是从对象的基地址开始偏移为0的内存
如果是非第一直接基类的指针
偏移就不是0了
而是要加上在其之前的基类的成员的size
回到正题
我们看vptr的值在不同的构造函数中是不同的
基类是0x1000010a8
派生类是0x100001060
中间有72bytes也就是9个ptr的间隔
这段空间是派生类vtbl所占用的
主要存储了重载的虚函数和type_info指针
对于每种编译器 vtbl的内容千差万别千奇百怪
一般都会有虚函数和type_info指针
感兴趣的朋友可以用示例代码中得到的vptr解引用得到的函数指针来遍历这段内存
看看你所用的编译器在里面究竟存了什么玩意儿
我在另一个问题里详细地讨论了继承模型的内存布局
问题地址
回到正题
这两个指针说明构造和析构会及时地修改vptr以匹配当前的类
由此证明了开头的论点:
不要在构造函数和析构函数中调用虚函数
因为基类构造函数执行时对象的vptr是指向基类的vtbl
而不是你所要创建的派生类的vtbl
因此你无法获得预期的虚函数多态
以关键字virtual的成员函数称为虚函数,主要是用于运行时多态,也就是动态绑定。
虚函数必须是类的成员函数,不能使友元函数、也不能是构造函数【原因:因为建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数,直到自己的构造函数,不能选择性的调用构造函数】
不能将虚函数说明为全局函数,也不能说明为static静态成员函数。因为虚函数的动态绑定必须在类的层次依靠this指针实现。
再添加一点:
虚函数的重载特性:一个派生类中定义基类的虚函数是函数重载的一种特殊形式。
重载一般的函数:函数的返回类型和参数的个数、类型可以不同,仅要求函数名相同;
而重载虚函数:要求函数名、返回类型、参数个数、参数类型和顺序都完全相同。
是在基类中说明的虚函数,它在基类中没有是在定义,要求所有派生类都必须定义自己的版本。
纯虚函数的定义形式:virtual 类型 函数名(参数表)=0,该函数赋值为0,表示没有实现定义。在基类中定义为0,在派生类中实现各自的版本。
纯虚函数是一种特殊的函数,它允许没有具体的实现
抽象类是指具有纯虚函数的类
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出
一个基类的说明中有纯虚函数,该基类的派生类一定不再是抽象类:该基类的派生类必须覆盖虚函数,这个函数才不是抽象类
纯虚函数与抽象类的关系:
抽象类中至少有一个纯虚函数。
如果抽象类中的派生类没有为基类的纯虚函数定义实现版本,那么它仍然是抽象类,相反,定义了纯虚函数的实现版本的派生类称为具体类。
抽象类在C++中有以下特点:
抽象类只能作为其他类的基类;
抽象类不能建立对象;
抽象类不能用作参数类型、参数返回类型或显示类型转换。
首先声明Base类型的指针指向实际类型为Derived的对象,先调用基类构造函数,再调用派生类构造函数。输出Base, Derived.
base->echo(); 指针是base类型,但是因为有关键词 virtual,所以不是隐藏而是重写.调用的是Derived的方法,输出Derived。
1.重载:重载从overload翻译过来,是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
2.隐藏:隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
3.重写:重写翻译自override,也翻译成覆盖(更好一点),是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
方法的重载:
方法重载的定义:同一个类或与他的派生类中,方法名相同,而参数列表不同的方法。其中参数列表不同指的是参数的类型,数量,类型的顺序这三种至少有一种不同。
方法重载与下列无关:
与返回值类型无关;与访问修饰符无关
构造方法也可以重载
方法的重写:
方法的重写的定义:在继承关系的子类中,定义一个与父类相同的方法
判断是否重写的方式:在方法之前加上@ Overri de
方法重写的特点:
在继承关系的子类中重写父类的方法
重写的方法必须方法名相同,参数列表也相同
重写的方法的返回值类型应该与父类中被重写方法的返回值类型相同或是他的子类类型
重写的方法的访问权限应该与父类中被重写方法的访问权限相同或高于它的访问权限
重写的方法不能抛出比父类更加宽泛的异常
方法重写的注意事项:
构造方法不能被重写,不要问为什么?因为构造方法名必须和类名相同
private修饰的成员方法不能被重写
static修饰的方法不能被重写
final修饰的方法不能被重写
当子类重写了父类中的方法后,子类对象调用该方法时调用的是子类重写后的方法
//首先将上面的语句分成两部分:
//前一部分是基类指针指向子类对象实现动态绑定,后一部分是new了一个子类对象;
//语句执行时,先执行后一部分new Derived()这句话,它会调用Derived() {echo();}
//而执行派生类的构造函数时,会先去执行基类的构造函数,所以会调用Base() {echo();},此时不涉及到虚函数的动态绑定,
//因为我们现在才执行了后一部分,还没有动态绑定,所以正常调用基类的echo()函数,执行完后返回到子类的构造函数,执行子类的echo();
//然后执行前一部分,实现虚函数的动态绑定。
base->echo();
//此时考察的就是虚函数的使用了,基类echo申明为虚函数,所以调用时会去子类寻找对应的虚函数执行。
a. 成员函数被重载的特征:
( 1 )相同的范围(在同一个类中);
( 2 )函数名字相同;
( 3 )参数不同;
( 4 ) virtual 关键字可有可无。
b. 覆盖是指派生类函数覆盖基类函数,特征是:
( 1 )不同的范围(分别位于派生类与基类);
( 2 )函数名字相同;
( 3 )参数相同;
( 4 )基类函数必须有 virtual 关键字。
c.“ 隐藏 ” 是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
( 1 )如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
( 2 )如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
1.malloc/free 是C/C++语言的标准库函数,new/delete是C++的运算符
2.new 能自动分配空间大小
3.对于用户自定义的对象而言,用malloc/free无法满足动态管理对象的要求
对象在创建的时候会自动调用构造函数,对象在消亡之前自动执行析构函数
由于malloc/free是库函数而不是运算符,不在编译器的控制范围,不能把构
造函数和析构函数的任务强加于malloc/free 。一次C++需要一个能够对对象完成动态分配内存和初始化工作的运算符new,以及一个释放内存的运算符
delete。简单来说就是new/delete能完成跟家详细的对内存的操作,而malloc/free不能。
C++语言将struct当成类来处理的,所以C++的struct可以包含C++类的所有东西,例如构造函数,析构函数,友元等,C++的struct和C++类唯一不同就是struct成员默认的是public, C++默认private。而C语言struct不是类,不可以有函数,也不能使用类的特征例如public等关键字 ,也不可以有static关键字。
这种题目是比较常见的题目,考点在于对charp="Hello’; char p[]字符串数组的理解。
若char p=“Hello”;实际上p是一个指针,指向的内容是字符串常量Hello,指向的内容是不可修改的!如果执行*p+1=‘F’操作是错误的!
因为p是指针,可以让p指向别的内容,如p=“china” 是正确的。
char p[]=“Hello”;实际上是一个字符串数组初始化的方式,字符串数组的有两种初始化方式,一种是这样的初始化,另外一种是列表初始化;
而p可以理解为是一个常量指针,p的值不可修改,如p++操作是错误的。
A.是最常见的一种初始化方式,这个没什么好说的。
B.p是一个指针,一开始指向的是一个字符数组a,后改变p指向了字符常量"china"
C.a是一个指针,指向了字符常量”china’’,是正确的。但是如果定义char a[10];a=“china”;是错误的!这相当于是数组的复制,而在c中并不存在数组复制的内建方法!
D.明显是错误的。
clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等
C++中的成员函数(构造函数,析构函数,虚函数等)都不占用类的空间的,之前学谭浩强的
如果类中存在虚函数,那么编译器会生成一个指针指向虚函数表VTable。32bit下,指针大小为4字节;64bit下,指针大小为8字节。
还有类中的静态数据成员,它不属于任何一个该类的实例化对象,可以通过类名+作用域直接访问,也不算在类的大小里。
类似于结构体大小计算时需要内存对齐,类大小的计算也需要经过内存对齐。
sizeof(类) = 非静态数据成员 + 虚函数表的指针 + 内存对齐
C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof(类)是4。
C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。 如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。
类中虚函数存放的只是虚函数表的指针,4个字节,加上char求组的3个字节,对齐以后就是8字节
构造函数
构造函数是一个特殊的公共成员函数,它在创建类对象时会自动被调用,用于构造类对象。如果程序员没有编写构造函数,则 C++ 会自动提供一个,这个自动提供的构造函数永远不会有人看到它,但是每当程序定义一个对象时,它会在后台静默运行。
当然,程序员在创建类时通常会编写自己的构造函数。如果这样做,那么除了构建类的每个新创建的对象之外,它还将执行程序员写入其中的其他任何代码。程序员通常使用构造函数来初始化对象的成员变量。但实际上,它可以做任何正常函数可以做的事情。
构造函数看起来像一个常规函数,除了它的名称必须与它所属类的名称相同,这就是为什么编译器知道这个特定的成员函数是一个构造函数。此外,构造函数不允许有返回类型。
析构函数
析构函数是具有与类相同名称的公共成员函数,前面带有波浪符号(〜)。例如,Rectangle 类的析构函数将被命名为 〜Rectangle。
当对象被销毁时,会自动调用析构函数。在创建对象时,构造函数使用某种方式来进行设置,那么当对象停止存在时,析构函数也会使用同样的方式来执行关闭过程。例如,当具有对象的程序停止执行或从创建对象的函数返回时,就会发生这种情况。
重载,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
析构函数没有返回值,也没有参数,没有函数类型,因此不能被重载
“拷贝构造函数”
概念
拷贝构造函数,又称复制构造函数,是一种特殊的 构造函数 ,它由 编译器 调用来完成一些基于同一类的其他对象的构建及初始化。. 其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。. 此函数经常用在 函数调用 时用户定义类型的值传递及返回。. 拷贝构造函数要调用 基类 的拷贝构造函数和成员函数。. 如果可以的话,它将用 常量 方式调用,另外,也可以用非常量方式调用。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数,其特征去下:
(1)拷贝构造函数是构造函数的一个重载形式。
(2)拷贝构造函数的参数只有一个且必须引用传参,使用传值方式会引发无穷递归调用。
可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
浅拷贝和深拷贝
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
CA(const CA& C)是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过CA(const CA& C)拷贝构造函数内的语句完成的。
私有成员对于类外部的所有程序部分而言都是隐藏的,访问它们需要调用一个公共成员函数,但有时也可能会需要创建该规则的一项例外。
友元函数是一个不属于类成员的函数,但它可以访问该类的私有成员。换句话说,友元函数被视为好像是该类的一个成员。友元函数可以是常规的独立函数,也可以是其他类的成员。实际上,整个类都可以声明为另一个类的友元。
为了使一个函数或类成为另一个类的友元,必须由授予它访问权限的类来声明。类保留了它们的朋友的 “名单”,只有名字出现在列表中的外部函数或类才被授予访问权限。通过将关键字 friend 放置在函数的原型之前,即可将函数声明为友元。
这四个运算符只能被重载为类的非静态成员函数,其他的可以被友元重载,主要是因为其他的运算符重载函数都会根据参数类型或数目进行精确匹配,这四个不具有这种检查的功能,用友元定义就会出错。
构造函数,拷贝构造函数,赋值函数,取值函数;即使用户没有显式定义拷贝构造函数,编译器也会隐式生成
const int *x; //①
int * const x; //②
const修饰离它最近的对象,
1)语句,const修饰的是const,指针变量x指向整型常数,x的值可以改变,但不能试图改变指向的整型常数;
2)语句,const修饰的是x,指针变量x的值不能改变。
const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。
即const在 * 的左边不能改变字符串常量的值,const在 * 的右边不能改变指针的指向
有以下说明语句:
struct point
{ int x; int y; }p;
则正确的赋值语句是( )。
共用体(联合)是一种同一存储区域由不同类型变量共享的数据类型。
结构体是用同一个名字引用的相关变量的集合。
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。正数的符号位为0,负数的符号位为1。
所以真值为-100101的数,其原码为10100101
补码的表示方法是:正数的补码就是其本身,负数的补码是在其原码的基础上, 符号位不变, 其余各位取反(也就是反码), 最后+1.
原码为10100101时,符号位不变其他位取反可以得到反码为11011010
求负数的补码 1.写出此数的绝对值的源码00100101(题目要求8位,所以前面补0) 2.按位取反11011010 3.加1 得11011011
本题考查类静态成员变量访问方法。静态成员变量又名类成员变量,为类所有,为所有类对象所有。
A选项:通过类对象访问,正确
B选项:通过类成员函数访问,正确,静态成员函数和非静态成员函数都可以直接访问
C选项:在静态成员函数中通过this指针访问,错误,this指针是指向当前类对象的指针,而fStatic是静态成员函数(是类的函数),没有this指针
D选项:通过类直接访问,正确
本题把a定义为静态数据成员
如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。
但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包含静态数据成员所占的空间。。静态数据成员是在所有对象之外单独开辟空间。
在一个类中可以有一个或多个静态数据成员,所有的对象都共享这些静态数据成员。都可以引用它
所以说静态成员函数并不属于某一个对象,它与任何对象都无关,因此静态成员函数没有this 指针
故综上所述C选项错误
malloc是库函数,而new是操作符
都是用来申请内存的
malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种
而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施给你使用:
当然,malloc并不是说比不上new,它们各自有适用的地方。在C++这种偏重OOP的语言,使用new/delete自然是更合适的
static 修饰的变量只初始化一次, 当下一次执行到这一条语句的时候,直接跳过
静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值。
静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见。
用法1:函数内部声明的static变量,可作为对象间的一种通信机制
如果一局部变量被声明为static,那么将只有唯一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量。这个对象将只在执行线程第一次到达它的定义使初始化。
用法2:局部静态对象
对于局部静态对象,构造函数是在控制线程第一次通过该对象的定义时调用。在程序结束时,局部静态对象的析构函数将按照他们被构造的相反顺序逐一调用,没有规定确切时间。
用法3:静态成员和静态成员函数
如果一个变量是类的一部分,但却不是该类的各个对象的一部分,它就被成为是一个static静态成员。一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本。同理,一个需要访问类成员,而不需要针对特定对象去调用的函数,也被称为一个static成员函数。
类的静态成员函数只能访问类的静态成员(变量或函数)。
A,静态成员属于类,而不是属于某个特定的对象,它是由该类的所有对象共享的,因此不能在类的构造方法中初始化
B,静态成员属于该类所有对象公有,可以被类对象调用
C,静态成员收private的限制
D,静态成员属于类和该类的所有对象,可以用类名直接调用
A:static数据成员在类的内部声明,但只能在类的外部定义,同时在定义时初始化,在类的外部不能再次指定static。(特例:当整型const static数据成员被常量表达式初始化时,就可以在类的内部声明时进行初始化);
B:静态数据成员属于类的所有实例(即对象),所有对象共享一份,在类被实例化时创建,通过类和对象都可以进行访问;
C:static成员实质上是加了“访问控制”的全局变量/函数,所以当然受访问控制符的控制;
D:同B的解析。
静态的使用注意事项:
1.静态方法只能访问静态成员(包括成员变量和成员方法)
非静态方法可以访问静态也可以访问非静态
2.静态方法中不可以定义this,super关键字
因为 一个类中,一个static变量只会有一个内存空间,虽然有多个类实例,但这些类实例中的这个static变量会共享同一个内存空间。静态方法在优先于对象存在,所以静态方法中不可以出现this,super关键字。
3.主函数是静态的。
程序运行的时候,静态成员已经加载在内存里面了,但是包含静态成员的对象共享这些静态成员,
比方说,A有一个静态成员public static int i;那么程序运行的时候,这个i就加载进内存了,A的所有对象的i变量都指向这个静态空间的i,也就是说创建对象之前,它就占空间了
若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度
若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度
静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出
a代表数组首行首地址,
a + i 代表第i行首地址,
*(a+i)+j 代表第i行第j个元素地址,
&&
表示逻辑与的意思,即为and。当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。
比如 12&&23的结果就是1,12&&-1 的结果是1,123&&0的结果就是0
&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals(“”))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException
&
表示按位与。
&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。
二进制与运算规则:1&1=1 1&0=0 0&0=0
15&127为什么等于15啊?
15二进制: (0000 1111)
127二进制: (1111 1111)
按位与自然就是(0000 1111)=15
||
表示逻辑或
逻辑或,是逻辑运算符,符号是“||”(在PASCAL中为"or")。 “逻辑或”相当于生活中的“或者”,当两个条件中有任一个条件满足,“逻辑或”的运算结果就为“真”
12||1 =1 12||0 =1 0||0 =0
|
表示按位或
按位或运算 按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位(也就是最后一位)相或。只要对应的二个二进位有一个为1时,结果位就为1。
128: (0001 0000 0000)
127: (0000 1111 1111) (高位用0补齐)
按位或就是(0001 1111 1111)=255
(1)数组指针:是一个叫做行指针的指针,int (*p) [n] , p是指向一个一维数组的指针,这个一维数组的长度是n,也就是说p的步长是n,即p+1,就是走了n个整型数据元素,比如:将二维数组赋值给这个指针
int a[3][4];
int (*p)[4]; //定义一个数组指针,指向包含4个int元素的数组
p = a; // 将二维数组的首地地址(a[0]或者&a[0][0])赋值给p
p++; //该语句执行后,p就跨越了4个 元素,也就是二维数组的一行,此时p指向的就是a[1][0]的地址
所以,数组指针也就是行指针
(2)指针数组:本质是数组,其中存放的是指针变量,int* p[n] ,表示数组p中存放了n个int类型的指针变量,这里的p+1是指向下一个元素的地址,步长是1个int元素,此时将二维数组赋值
int *p[3];
int a[3][4];
for(i=0;i<3;i++)
p[i]=a[i] //分别将a[0][0],a[1][0],a[2][0]的地址赋值给p[0],p[1],p[2]这三个指针
对程序进行重定位的技术按重定位的时机可分为两种:静态重定位和动态重定位。
静态重定位:是在目标程序装入内存时,由装入程序对目标程序中的指令和数据的地址进行修改,即把程序的逻辑地址都改成实际的地址。对每个程序来说,这种地址变换只是在装入时一次完成,在程序运行期间不再进行重定位。
优点:是无需增加硬件地址转换机构,便于实现程序的静态连接。在早期计算机系统中大多采用这种方案。
缺点:(1)程序的存储空间只能是连续的一片区域,而且在重定位之后就不能再移动。这不利于内存空间的有效使用。(2)各个用户进程很难共享内存中的同一程序的副本。
动态重定位:是在程序执行期间每次访问内存之前进行重定位。这种变换是靠硬件地址变换机构实现的。通常采用一个重定位寄存器,其中放有当前正在执行的程序在内存空间中的起始地址,而地址空间中的代码在装入过程中不发生变化。
优点:(1)程序占用的内存空间动态可变,不必连续存放在一处。(2)比较容易实现几个进程对同一程序副本的共享使用。
缺点:是需要附加的硬件支持,增加了机器成本,而且实现存储管理的软件算法比较复杂。
现在一般计算机系统中都采用动态重定位方法。
1> malloc,作用是开辟一个给定字节大小的堆区域空间,并且返回该内存空间的首地址。
void *malloc(unsigned int size);因此A项正确。
2> calloc,作用是分配n个size⼤⼩的空间,并且把该内存上的所有字节清零。
void *calloc(unsigned n,unsigned size);
3> realloc,作用是按给定的地址以及给定的⼤小重新分配。
void *realloc(void *, unsigned newSize);
分配时有两种情况:
1.如果原有空间地址后面还有足够的空闲空间用来分配,则将先前空间释放,然后以先前地址为开始地址按newSize大小重新分配。
2.如果原有空间地址后面没有足够的空闲空间用来分配,那么从堆中另外找一块newsize⼤小的内存,并把先前内存空间中的数据复制到新的newSize⼤小的空间中,然后将之前空间释放。
4> free函数,作用是释放内存,内存释放是标记删除, 只会修改当前空间的所属状态,并不会清除空间内容。free函数并不会清除空间内容。
A中,a占4个字节,b占1个字节,但因为c占4个字节,为了满足条件2,b需要占用4个字节,为了满足条件1,d需要占用4个字节,所以一共是16个字节;
B中,a占用4个字节,b占用2个字节,d占用1个字节,但因为c占用4个字节,为了满足条件2,d需要占用2个字节,即abd共占用8个字节,加上c的4个字节,一共12个字节;
结构体的调用顺序是根据类中声明的顺序来的,本题中先B b, A a,所以先输出B,然后输出A,,最后再输出C
1个指令周期 = n个机器周期
1个机器周期 = n 个时钟周期(也称为振荡周期)
时钟周期:晶振频率的倒数
机器周期 :在计算机中,为了便于管理,常把一条指令的执行过程划分为若干个阶段,每一阶段完成一项工作。例如,取指令、存储器读、存储器写等,这每一项工作称为一个基本操作。完成一个基本操作所需要的时间称为机器周期。
指令周期 :指取出并完成一条指令所需的时间
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
请看下面的实例,了解 C++ 中运算符的优先级。
复制并黏贴下面的 C++ 程序到 test.cpp 文件中,编译并运行程序。
对比有括号和没有括号时的区别,这将产生不同的结果。因为 ()、 /、 * 和 + 有不同的优先级,高优先级的操作符将优先计算。
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
内联函数inline:引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.