C++面试笔试题目(选1)

标签(空格分隔): 未分类

看网上文章的同时,把里面提到的一些基础问题重新按照自己的话来写一遍,加深印象。

1.new、delete、malloc、free 的关系

  • new/delete是运算符,malloc/free是标准库函数
  • 对于非内部数据类型的对象,malloc/free是无法满足要求的
  • new会调用构造函数,delete会调用析构函数

2.引用

  • 不能建立引用数组
  • 声明引用一定要初始化
  • 使用引用穿点函数的参数,内存中并没有产生实参的副本,如果传递的是对象,还将调用拷贝构造函数,因此传递的参数数据较大的时候,一般使用引用来传递
  • 当函数的返回值为引用时,不能反悔局部变量的引用,不能返回函数内部new分配的内存的引用
  • 引用是除指针外另一个可以产生多台效果的手段:
    Class A; Class B : Class A{...}; B b; A& ref = b;
  • 如下代码的结果是? 答案30
    #define DOUBLE(x) x+x
    cout<<5*DOUBLE(5);

    他与“#define DOUBLE(x) (x+x) ”不相同

3.struct和class的区别

  • struct默认的成员是public,class默认是private
  • struct可以继承class,同样class也可以继承struct
  • 私有继承函数公有继承?
    struct A{};class B : A{}; //private继承
    struct C : B{}; //public继承

4.内存分配问题

c++中,内存分为5个区,分别为:堆、栈、自由存储区、全局/静态存储区、常量存储区。

  • ,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
  • ,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)

5.位、字节、字

  • 位(bit)0或者1
  • 字节(byte)8个二进制位组成一个字节
  • 字(word)由若干个字节构成,若一台16位机,他的字就由2个字节构成,字长为16位。
  • 1 kb = 1024 byte

在32位编译器下:
short int 2字节
int 4字节
unsigned int 4字节
char 1字节
char * 4字节(即指针变量): 4个字节(32位的寻址空间是2^32,即32个bit,也就是4个字节。同理64位编译器)
float 4字节
double 8字节
long 4字节
long long 8字节

6.自增自减操作

int i =0, j;
j = ++i; //前自增 j = 1, i = 1
j = i++; //后自增 j = 1, i = 2

7.预处理机制

预处理是指在进行编译的第一遍扫描之前做的工作,预处理有预处理程序负责完成
宏定义是C语言开始提供的3种预处理功能之一,这3种预处理分别是:宏定义、文件包含、条件编译
宏定义与操作符的区别:宏定义是替换,不计算,也不做表达式求解,另外宏定义替换在编译前进行,不占用内存,宏的展开不占用运行时间,只占编译时间,而操作符占用运行时间。

  • define中的三个特殊符号:#,##,#@

#define Conn(x,y) x##y
#define ToChar(x) #@x
#define ToString(x) #x

x##y 表示x连接y
int n = Conn(123,456); /* 结果就是n=123456;*/
char* str = Conn("asdf", "adf"); /*结果就是 str = "asdfadf";*/

#@x表示给x加上单引号,返回一个const char。
char a = ToChar(1);//结果就是a='1';
做个越界试验char a = ToChar(123);结果就错了;
但是如果你的参数超过四个字符,编译器就给给你报错了!
error C2015: too many characters in constant :P

#x表示给x加双引号
char* str = ToString(123132);//就成了str="123132";

参考文章 C++宏定义详解

8.虚函数与纯虚函数

虚函数的主要作用是建立抽象模型,从而达到方便扩展系统的目的,纯虚函数是指被表明为不具体实现的虚函数,是一种特殊的虚函数

很多情况下,基类中不对虚函数给出有意义的实现,而是把它声明为纯虚函数,它的实现留给该基类的派生类来做,这就是纯虚函数的作用

int*(*fn)(int*)=0;上述代码什么意思?
代表是返回类型是int*的,输入参数是int*的纯虚函数指针。
int*: 返回值类型
(*fn): 指针函数
(int*): 参数类型
=0: 纯虚函数

9.const

  • 指向const对象的指针是指指针指向const对象的地址,可以使用指针来修改所指对象的值。

const double *cptr;//cptr指针可能指向一个类型为double的常量值

  • const指针
    int errNum = 0;
    int * const curErr = & errNum;//curErr 将一直指向errNum

10.函数指针

例子:int (*f)(int x);
由于()运算符的优先级高于*,所以指针变量名外的括号必不可少。
int func(int x);//声明一个函数
int (*f) (int x);//声明一个函数指针
f = func;//将func()函数的首地址复制给指针f

注意赋值的时候func不带括号,也不带参数,由于func代表函数的首地址,因此复制以后,指针f就指向函数func(x)的代码的首地址。

11.抽象类

抽象类可以提供多个派生类共享基类的公共定义,他可以提供抽象方法,也可以提供非抽象方法。抽象类不鞥呢被实例化,必须通过继承由派生类实现其抽象方法,也就是说,对抽象类不能使用new 关键字,也不能被封装。 如果抽象类的派生类没有实现所有的抽象方法,则该派生类也必须声明为抽象类。派生类使用覆盖来实现抽象方法。
抽象类一定包含有纯虚函数,因此不能定义抽象类对象。

  • 基类中的析构函数一般为虚析构函数,因为在释放这个对象并进行内存释放时,要将基类和派生类中两部分申请的内存空间都要释放掉,否则只会调用基类的析构函数,而派生类的析构函数并不会调用,会造成内存泄露。参考:C++中虚析构函数的作用

另外,纯虚析构函数在声明之后一定要定义,派生类的析构函数会自动调用其基类的析构函数。这个过程是递归的,最终,抽象类的纯虚析构函数也会被调用。
如果纯虚析构函数只被声明而没有定义,那么就会造成运行时(runtime)崩溃。(在很多情况下,这个错误会出现在编译期,但谁也不担保一定会是这样。)纯虚析构函数的哑元实现(dummy implementation,即空实现)能够保证这样的代码的安全性。
参考: C++虚析构函数、纯虚析构函数

其实还有些基础的东西需要了解,那就是虚函数表
虚函数表(Virtual Function)是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。
更多请参考:C++ 虚函数表解析

  • 构造函数为啥不能是虚函数?

    1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

    2. 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

    3. 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

    4. 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。

    5. 当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。

你可能感兴趣的:(C语言)