91、C++的值传递、指针传递和引用传递?
值传递: 是最普通的传递方式,如函数定义为fun(int a),在调用的地方有int x= 1,使用fun(x)即可。这种方式在fun(int a)函数内部的对a的修改不能导致外部x的变化。
指针传递: 是地址传递,函数定义为fun(int *a),形参为指针,要求调用的时候传递进去一个参数的地址,例如int x = 1; fun(&x)。这种方式在fun(int *a)函数内部的对a的修改能导致外部x的变化。
引用传递: 只有C++支持,引用传递函数定义为fun(int &a),这里&符号是引用而不是取地址的意思,调用方式和值传递类似,例如int x= 1; fun(x)。但是这种方式在fun(int a)函数内部的对a的修改能导致外部x的变化。
区别:
- 指针参数传递本质上是值传递,传递的是一个地址值。
- 值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本;
- 值传递特点: 被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
- 引用参数传递过程中,被调函数的形式参数也作为局部变量,在栈中开辟了内存空间,但这时存放的是由主调函数放进来的实参变量的地址。
- 被调函数对形参(本体)的任何操作都被处理成间接寻址,通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
- 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但任何对于引用参数的处理,都会通过一个间接寻址的方式操作到主调函数中的相关变量。
- 而对于指针传递的参数,如果改变被调函数中的指针地址,它将用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
92、类如何实现只能静态分配和只能动态分配
静态分配: 把new、delete运算符重载为private属性。
动态分配: 把构造、析构函数设为protected属性,再用子类来动态创建。
建立类的对象有两种方式:
- 静态建立: 静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
- 动态建立:
A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;
只有使用new运算符,对象才会被建立在堆上;因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。
93、如果想将某个类用作基类,为什么该类必须定义而非声明?
- 派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。
- 所以必须定义而非声明。
94、继承机制中对象之间如何转换?指针和引用之间如何转换?
1、向上类型转换
- 将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。
2、向下类型转换
- 将基类指针或引用转换为派生类指针或引用被称为向下类型转换;
- 向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。
- RTTI技术,用dynamic_cast进行向下类型转换。
95、知道C++中的组合吗?它与继承相比有什么优缺点吗?
1、继承
- 继承是is a 的关系,比如说Student继承Person,则说明Student is a Person。
- 继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
- 父类的内部细节对子类是可见的;
- 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为;
- 如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
2、组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点:
- 当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
- 当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码,没有影响。
组合的缺点:
- 容易产生过多的对象。
- 为了能组合多个对象,必须仔细对接口进行定义。
96、函数指针?
什么是函数指针?
- 函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。
函数指针的声明方法
int (*pf)(const int&, const int&);
上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。
注意*pf两边的括号是必须的。
为什么有函数指针?
- 函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。
97、说一说你理解的内存对齐
- 分配内存的顺序是按照声明的顺序。
- 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
- 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
98、结构体变量比较是否相等
1、重载了 “==” 操作符
struct foo {
int a;
int b;
bool operator==(const foo& rhs) *
{
return( a == rhs.a) && (b == rhs.b);
}
};
2、元素的话,一个个比;
3、指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;
99、define、const、typedef、inline的使用方法?他们之间有什么区别?
const与#define的区别:
- const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
- define只在预处理阶段起作用,简单的文本替换,而const在编译、链接过程中起作用;
- define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
- define预处理后,占用代码段空间,const占用数据段空间;
- const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
- define独特功能,比如可以用来防止文件重复引用。
#define和别名typedef的区别:
- 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
- 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
- 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。
define与inline的区别:
- #define是关键字,inline是函数;
- define宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
- inline函数有类型检查,相比define宏定义比较安全;
100、你知道printf函数的实现原理是什么吗?
- 在C/C++中,对函数参数的扫描是从后向前的。
- C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构)
- 在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。
- printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,printf(“%d,%d”,a,b);(其中a、b都是int型的)