图像算法工程面试所做的准备——C++基础篇

发布时间:2023-08-01 18:30

在面试过程中遇到C++相关的问题,有的也是网上搜索的总结,有的贴出原文链接,有的没有贴

1、虚函数和纯虚函数的区别:

 

答:虚函数为了重载和多态的需要,在基类中是有实现的,virtual void Eat(){……};所以子类中可以重载也可以不重载基类中的此函数   

纯虚函数就是基类只定义了函数体,没有实现过程,定义方法如: virtual void Eat() = 0; 纯虚函数相当于Java中的接口,自己不去实现,需要子类来实现函数定义;带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重载其虚函数后,才能使用。这样的类也叫抽象类。

2、内联函数和宏定义的差别是什么:

1)内联函数在编译时展开,宏在预编译时展开;

  (2)内联函数直接嵌入到目标代码中,宏是简单的做文本替换;

  (3)内联函数有类型检测、语法判断等功能,而宏没有;

  (4)inline函数是函数,宏不是;

3、重载、继承、多肽都有什么区别:

函数重载:函数名相同,参数不同

对于重载的多个函数来说,其函数名都是一样的,为了加以区分,在编译连接时,C++会按照自己的规则篡改函数名字,这一过程为\"名字改编\".有的书中也称为\"名字粉碎\".不同的C++编译器会采用不同的规则进行名字改编

也有运算符重载,实现运算符的加减乘除;

继承:子类可以具有父类的特性,就是继承;但是基类中私有成员无法被继承。

一个派生类继承了所有的基类方法,但下列情况除外:

基类的构造函数、析构函数和拷贝构造函数。

基类的重载运算符。

基类的友元函数(我们说过友元函数不是成员函数)。

有三种继承方式:

公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员(指的是函数)来访问。

保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。(注意:公有成员也变成了保护成员)

私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。(一般都不会使用这种)

封装:

封装的思想就是将实现的细节隐藏,而暴露公有接口;

C++中的访问标识符,可以实现在类中的封装;通常是将所有的成员变量私有化

尽管看起来访问成员变量的不直接,但使程序更有可重用性和可维护性;

B)隐藏了类的实现,类的使用者只需知道公共的接口,就可以使用该类;

C)封装帮助防止意外的改变和误用;

D)对程序调试有很大的帮助,因为改变类的成员变量只用通过公共接口。

封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用,而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

多态:指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性(也叫静态联编),运行时多态性(动态联编)。 

C++支持两种形式的多态性。第一种是编译时的多态性,称为静态联编。第二种是运行时的多态性,也称为动态联编。运行时的多态性是指必须在运行中才可以确定的多态性,是通过继承和虚函数来实现的。
  a、编译时多态性:通过重载函数实现   采用早绑定:就是在编译时确定对象调用的函数的地址(一般运用于重载)函数多态性
  b、运行时多态性:通过虚函数实现。  采用迟绑定:就会在运行时再去确定对象的类型以及正确的调用函数类的多态性在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数,此为多态的表现;)( 动态联编要求派生类中的虚函数与基类中对应的虚函数具有相同的名称、相同的参数个数和相同的对应参数类型、返回值或者相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中虚函数所返回的指针或引用的基类型的子类型。如果不满足这些条件,派生类中的虚函数将丢失其虚特性,在调用时进行静态联编。参考:http://blog.csdn.net/neiloid/article/details/6934129

4、预处理和链接库:

答:一个C++程序一般需要经过以下几个步骤才能成为可执行程序:

C++源程序——》编译器预处理——》编译程序——》优化程序——》汇编程序——》链接程序——》可执行程序

编译器预处理:C++编译器自带预处理器,在程序编译之前,由预处理器对C++源程序完成预处理工作。

预处理主要将源程序中的宏定义指令、条件编译指令、头文件包含指令以及特殊符号完成相应的替换工作。

编译程序:以预编译的输出作为输入,利用C++运行库,通过词法分析和语法分析,

在确认所有的指令都符合语法规则时,将其翻译成等价的中间代码表示或者是汇编语言。

优化程序:优化阶段一部分是对中间代码的优化,这种优化不依赖具体的计算机,同机器的硬件环境无关。另一种优化则主要针对目标代码的生成而进行的。对于前一种优化,主要的工作是删除公共表达式、循环优化(循环展开,自动向量化、循环不变量代码移动)以及无用赋值的删除等。

另一种优化同机器的硬件结构密切相关。最主要的是考虑如何充分利用机器的各个硬件寄存器存放有关的变量的值,以减少对于内存的访问次数。

汇编程序:汇编阶段的主要工作是将经过编译、优化后的,以汇编语言的形式存在的程序转化为机器可识别的二进制代码,从而得到相应的目标文件。

链接程序:经历了汇编之后的程序是后缀为.obj形式的文件,仍然是不可执行的,只有经过链接阶段,程序所引用的外部文件关联起来之后,形成.exe后缀的文件之后,才是可执行的。程序中可能引用了定义在其他外部文件中的变量或者函数,比如某些库函数,而链接阶段所做的主要事情就是将这些相关联的文件链接起来,使得所有这些目标文件成为一个能够被操作系统装入执行的统一的整体。

 

动态链接:采用该链接方式表明,需要链接的代码是存放在动态链接库或者某个共享对象的目标文件。

静态链接:采用该链接方式,需要链接的代码会被链接程序从相应的静态链接库中拷贝到可执行程序之中。

5、c++多态底层实现原理

编译器在编译的时候,发现类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址。对于基类类都包含了一个虚函数breathe(),因此编译器会为这两个类都建立一个虚表,(即使子类里面没有virtual函数,但是其父类里面有,所以子类中也有了)如下图所示:  

           

那么如何定位虚表呢?编译器另外还为每个类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。
     正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢?
      要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。


    对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。

   需要注意的几点
   总结(基类有虚函数):
     1、每一个类都有虚表。
     2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
     3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

6、指针(pointer)和引用(Reference)的区别:

函数调用时,什么时候采用值传递,什么时候采用引用传递:


如果数据对象很小,如内置数据类型或小型结构,则按值传递。

如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。

如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。

如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:

如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int型),则很明显,该函数将修改x。

如果数据对象是数组,则只能使用指针。

如果数据对象是结构,则使用引用或指针。

如果数据对象是类对象,则使用引用。

 

指针和引用的定义和性质区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。

int a=1;int *p=&a;

int a=1;int &b=a;

(2)可以有const指针,但是没有const引用

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)\"sizeof引用\"得到的是所指向的变量(对象)的大小,而\"sizeof指针\"得到的是指针本身的大小

(7)指针和引用的自增(++)运算意义不一样;

7、为什么构造函数不能声明为虚函数、析构函数可以

构造函数不能声明为虚函数的原因是:why
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。

2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。参考:http://www.cnblogs.com/children/archive/2012/08/13/2636956.html

什么叫基类指针指向派生类:举个例子,定义一个基类Person Student继承Person

Person *pt1 = new Person;
Person *pt2 = new Student;          // 用基类的指针指向子类
// Student *pt3 = new Person;     // 不能用子类指针指向基类,错误!
Student *pt4 = new Student;

这个时候delete *pt1 会直接调用Person 的析构函数

delete *pt2,如果Person的析构函数声明为virtual类型,则会先调用Student的析构函数,再调用Person的析构函数,如果Person的析构函数声明不是virtual类型,则只会调用基类Person的析构函数,不会调用Student的析构函数,这样就会导致销毁对象不完全。

结论:最好对着上面链接的例子理解:

可以看出:只有在用基类的指针指向派生类的时候,才会出现这种情况。(ps:为什么只有这种情况呢,在基类中定义了一个虚函数,然后在派生类中又定义一个同名,同参数表的函数,这就是多态。通过一个基类指针来操作对象,如果对象是基类对象,就会调用基类中的那个函数,如果对象实际是派生类对象,就会调用派声类中的那个函数,调用哪个函数并不由函数的参数表决定,而是由函数的实际类型决定。)因为这个时候虚函数发挥了动态的作用。

析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数。如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用delete销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全。

如果在上面的例子中,基类Person中未定义virtual析构函数,而派生类Student中定义了virtual的析构函数,此时用基类指针指向派生类,再delete掉,运行结果会出错。如果再定义一个类OneSt继承Student则不会出现错误,因为Student作为基类了。

Effective C++ (第7条:要将多态基类的析构函数声明为虚函数)

需要记住的

应该为多态基类声明虚析构器。一旦一个类包含虚函数,它就应该包含一个虚析构器。

如果一个类不用作基类或者不需具有多态性,便不应该为它声明虚析构器。

基类指针指向派生类的例子,这个很容易考:

参考:http://blog.csdn.net/dongyanxia1000/article/details/50466729

基类的指针指向派生类的对象,当调用同名的成员函数时:

1)如果在基类中成员函数为虚函数,那么基类指针调用的就是派生类的同名函数。virtual void display();

     可以这么理解:因为该函数是虚的,所以会找真正实现的那个函数,所以调用派生类B中的 B class virtual display.

2)如果基类中成员函数为非虚函数,则调用的是基类的成员函数。void show();

     因为基类是非虚的,已经完全实现了,所以没有必要再调用派生类的了,就调用基类的A class show()

 

8、C++四中cast操作符:

 

参考:http://blog.csdn.net/starryheavens/article/details/4617637

http://blog.csdn.net/shun_fzll/article/details/38358195

 

c语言中强制转换符:int a; (float)a;

因为c风格的类型转换有不少的缺点,它可以在任意类型之间转换,比如你可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成指向一个派生类对象的指针,这两种转换之间的差别是巨大的,但是传统的c语言风格的类型转换没有区分这些。还有一个缺点就是,c风格的转换不容易查找,他由一个括号加上一个标识符组成,而这样的东西在c++程序里一大堆。所以c++为了克服这些缺点,引进了4种新的类型转换操作符。

对于c++来说,C++标准定义了四个新的转换符:reinterpret_cast, static_cast, dynamic_cast和const_cast,目的在于控制类(class)之间的类型转换。

1、reinterpret_cast

用法:reinpreter_cast (expression)

type-id必须是一个指针、引用、算术类型函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。

这个操作符能够在非相关的类型之间转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。reinpreter_cast是特意用于底层的强制转型,导致实现依赖(就是说,不可移植)的结果。

Int  n=9;

// reinterpret_cast 仅仅是复制 n 的比特位到 d,因此d 包含无用值。

double d=reinterpret_cast (n);

2、const_cast

用法:const_cast (expression)

用于修改类型的const或volatile属性。除了const 或volatile修饰之外,type_id和expression的类型是一样的,一般用于强制消除对象的常量性它是唯一能做到这一点的 C++ 风格的强制转型,而C不提供消除const的机制(已验证)。

常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

 

3、static_cast

 

最常用的转换,C语言类型的转化,但是转换的时候不会检查类型来保证转换的安全性

1)用于基本数据类型之间的转换,如把int转换成char,non-const 对象转型为 const 对象(这里相反方向不可以,C++只有const_cast可以)。

2)把空指针转换成目标类型的指针。(之前的做法是用强制转换(type-id*)

3)把任何类型的表达式转换成void类型

4)应用到类的指针上,它允许子类类型的指针转换为父类类型的指针(upercasting这是一个有效的隐式转换);也能够执行相反动作,即转换父类为它的子类(downcasting),这种转换的安全性需要开发人员来保证(主要是在非上下转型中)。

class Base {};

class Derived : public Base {};

Base *a = new Base;

Derived *b = NULL;

b = static_cast(a); //可以通过编译,但存在安全隐患(如访问//Derived的成员)

 

注意:

1static_cast不能转换掉expressionconstvolitale、或者__unaligned属性。

2.在非基本类型或上下转型中,被转换的父类需要检查是否与目的类型相一致,否则,如果在两个完全不相干的类之间进行转换,将会导致编译出错。

 

4、dynamic_cast

Dynamic_cast 与其他操作符不同,涉及运行类型检验,如果绑定到指针或引用上的对象不是目标类型的对象,则dynamic_cast 失败,如果转换到指针类型的dynamic_cast失败,则结果为0,如果转换到引用类型的dynamic_cast 失败,则抛出bad_cast异常。

因此,dynamic_cast操作符一次执行两个操作它首先验证被请求的转换是否有效只有转换有效,操作符才实际进行转换。由于引用或指针在所绑定的对象类型在编译时时未知的,所以dynamic_cast执行的验证只有在运行时进行。

注意:

(1)T必须是类的指针、类的引用或者void *。如果T是类指针类型,那么expression也必须是一个指针,如果T是一个引用,那么expression也必须是一个引用。

(2)dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换(子类->子类/基类)时,dynamic_cast和static_cast的效果是一样的;在进行下行转换(基类->子类)时,dynamic_cast具有类型检查的功能,比static_cast更安全。

(2)dynamic_cast转换符只能用于含有虚函数的类。

也就是说只有基类的指针指向派生类时,这种基类和子类之间的转换才算成功

举例:

class Base { virtual dummy() {} };

class Derived : public Base {};

class Other{} ;

Base* b1 = new Derived;

Base* b2 = new Base;

Derived* d1 = dynamic_cast(b1);  // succeeds

Derived* d2 = dynamic_cast(b2);  // fails: returns \'NULL\'

//如果一个引用类型执行了类型转换并且这个转换是不可能的,运行时一个//bad_cast的异常类型会被抛出:

Derived d3 = dynamic_cast(*b1);  // succeeds

Derived d4 = dynamic_cast(*b2);  // fails: exception thrown

注意:Base需要有虚函数,否则会编译出错。

 

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换(子类->子类/基类)时,dynamic_cast和static_cast的效果是一样的;在进行下行转换(基类->子类)时,dynamic_cast具有类型检查的功能,比static_cast更安全。

 

9、智能指针,以及其原理和原因:

 

参考:http://www.cnblogs.com/lanxuezaipiao/p/4132096.html

   智能指针: 智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。

智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。当然,智能指针还不止这些,还包括复制时可以修改源对象等。智能指针根据需求不同,设计也不同

智能指针采用计数器的用法:

智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象

STL常用的4种智能指针类型为:auto_ptr、unique_ptr、shared_ptr和weak_ptr

(1) 为什么现在不用auto_ptr了:为什么现在不用auto_ptr了,因为auto_ptr建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。

(2) 为什么unique_ptr比auto_ptr更严格:

举个例子:auto_ptr<string> p1(new string (\"auto\") ; //#1

auto_ptr<string> p2;                      //#2

p2 = p1;                                   //#3

对于语句#3,auto_ptr 会执行,p2接管string对象的所有权后,p1的所有权将被剥夺。但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。但是unique_ptr认为#3语句是非法的,所以不再执行语句3避免了p1不再指向有效数据的问题。

(3) 但是对于unique_ptr怎么实现赋值操作呢?

可以采用调用函数的方式返回临时变量的方式进行赋值,如下所示:

unique_ptr demo(const char * s)

{

unique_ptr temp (new string (s));

return temp;

}

unique_ptr<string> ps;

ps = demo(\'Uniquely special\");

demo() 返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。

(4) 但是比较常用的还是shared_ptr指针

  因为shared_ptr的出现就是为了解决所有权转移出现的问题,采用引用计数器的方法,shared_ptr内部用一个计数器来确定对象被引用的次数。引用的时候计数器增加1,当然这个引用计数也是在堆上分配的。确保指向同一个对象的引用计数是同一块内存。取消引用的时候计数器减1计数器减到0的时候就删除这个对象。

 

10、深拷贝和浅拷贝的区别:

:在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误

11、关键字static的作用是什么?

C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用

一、面向过程设计中的static
1、静态全局变量该变量在全局数据区分配内存;未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的

2、静态局部变量:1)该变量在全局数据区分配内存;
    (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
    (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
    (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

3、静态函数:定义静态函数的好处:
       静态函数不能被其它文件所用
       其它文件中可以定义相同名字的函数,不会发生冲突;

二、面向对象的static关键字(类中的static关键字)

1、静态成员数据:在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。

2、静态成员函数:与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。

12、关键字const的作用

在C程序中,const的用法主要有定义常量、修饰函数参数和修饰函数返回值。而在C++程序中,它除了上述功能外,还可以修饰函数的定义体,定义类中某个成员函数为恒态函数,即不改变类中的数据成员。对于定义常量的用法,这里就不多说了,重点看一下修饰函数参数、修饰函数返回值以及修饰函数的定义体。

一、修饰变量或指针

int ii=0; 
const int i=0;           //i是常量,i的值不会被修改 
const int *p1i=&i;        //指针p1i所指内容是常量,可以不初始化 

int const* p11i = &i; //同上,const在*号左边,作用是一样的。
int  * const p2i=ⅈ    //指针p2i是常量,所指内容可修改 
const int * const p3i=&i; //指针p3i是常量,所指内容也是常量 

二、修饰函数参数

    首先如果该参数用于输出,那么无论是采用指针传递还是引用传递,都不能加const修饰。所以const只能用于修饰输入参数。这里又分三种情况:输入参数采用值传递还是指针传递还是引用传递。

(1)如果采用值传递

由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不需要加const 修饰。例如,对于函数void Func1(int x),写成void Func1(const int x)一点意义也没有。同理,对于void Func2(A a)也不需要写成void Func2(const A a),其中A为用户自定义的对象类型。

(2)如果采用指针传递,那么加const可以防止函数体内部对该参数进行改变,起到保护作用。

例如,假设StringCopy函数定义为:void StringCopy(char *strDest, const char *strSrc),那么,如果函数体试图改变strSrc的内容,编译器将报错。    

(3)如果采用引用传递

     首先我们来说一下,为什么要引入引用传递这种方法。原因是:对于非内部数据类型的参数而言,像void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。为了提高效率,可以将函数声明改为void Func(A& a)。这样一来,根据引用传递的定义,只是借用了参数的别名,不需要产生临时对象。

三、修饰函数的定义体。

       定义const函数,只需要将const关键字放在函数声明的尾部。任何不会修改类的数据成员的函数都应该声明为const 类型。如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将报错,这无疑会提高程序的健壮性

13、 简述C,C++程序编译的内存分配情况?

解:C,C++中内存分配方式可以分为三种:

1. 从静态存储区域分配:内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快,不容易出错,因有系统自行管理。

2. 在栈上分配:在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

3. 从堆上分配:即运态内存分配。程序在运行时候用malloc或new申请任意大小的内存,程序员自己负责在何进用free 和delete释放内存。

一个C、C++程序编译时内存分为5大存储区:堆区、栈区、全局区、文字常量区和程序代码区。

 

14、初始化列表c++:

除了性能问题之外,有些时场合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表

· 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面

· 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

· 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。

 

 

15.new malloc 的区别,问的比较深

 

1、本质区别:new /delete是c++中的操作符,malloc/free 是 C/C++语言的标准库函数

2、new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数

3、用法上不同:

用malloc 申请一块长度为length 的整数类型的内存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);

对于malloc 主要的两点是:“类型转换”“sizeof”

而malloc返回的都是void指针,所以需要强制转换

malloc 函数返回的是 void * 类型。对于C++,如果你写成:p = malloc (sizeof(int)); 则程序无法通过编译,报错:”不能将 void* 赋值给 int * 类型变量。所以必须通过 (int *) 来将强制转换。而对于C,没有这个要求,但为了使C程序更方便的移植到C++中来,建议养成强制转换的习惯。

int* p;

p = (int *) malloc (sizeof(int)*128);

//分配128个(可根据实际需要替换该数值)整型存储单元,

//并将这128个连续的整型存储单元的首地址存储到指针变量p中

 

而对于new比较简单,直接可以 Obj *objects = new Obj[100];     这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。


3、内存泄漏对于malloc或者new都可以检查出来的,区别在于new可以指明是那个文件的那一行,而malloc没有这些信息。new可以说出错误的具体问题,而malloc不能
4、new 和 malloc效率比较
   new可以认为是malloc加构造函数的执行。
   new出来的指针是直接带类型信息的

Malloc申请空间的原理讲的比较清晰

malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

16、_stdcall,_cdecl区别

(1) _stdcall调用

    _stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。WIN32 Api都采用_stdcall调用方式,这样的宏定义说明了问题:

#define WINAPI _stdcall

  按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数。

(2) _cdecl调用

_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。 

17、实现strcpy公式:

(1) char  * strcpy ( char  * strDest ,  const   char  * strSrc )

{

     assert (( strDest != NULL ) && ( strSrc  != NULL ));//这个必须要啊,判断指针是否为空

     char  * address  =  strDest ;                                        

     while ( (* strDest ++ = *  strSrc ++) !=  \'\\0\'  )         

               NULL  ;

     return   address  ;                                                

}

 2  strcpy 能把 strSrc 的内容复制到 strDest ,为什么还要 char *  类型的返回值?

答:为了 实现链式表达 式。  

18、文件声明的区别

#include系统检索头文件时,会先从系统文件里开始找,再找其他地方,用于系统文件较快。

#include “filename.h”系统检索文件时,先从程序所处目录开始查找,用于自定义文件比较快。

ifndef/define/endif 的含义:如果未定义 / 那么定义 / 完成假设。一般是用来防止头文件被重复包含,提高编译效率的。

 

19、map是用什么实现的,插入数据的方式有哪几种,有什么不同

 

C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树,所以在存储map时是默认有序的。

在map和set中查找是使用二分查找,所以时间复杂度为Olog(n)

插入数据的模式有三种:

(1)   my_Map.insert(map::value_type(\"b\",2)); 
(2)   my_Map.insert(make_pair(\"d\",4))

(3)   my_Map[\"a\"]   =   1;  //我经常用插入数据的方法,居然忘了

以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的 插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是插入数据不了的,但是用数组方式就不同了,它可以覆盖以前该关键字对 应的值.

那怎么判断是否插入成功了呢?

mapStudent.insert(map::value_type (1, \"student_one\"));

 mapStudent.insert(map::value_type (1, \"student_two\"));

 

上面这两条语句执行后,map中1这个关键字对应的值是“student_one”,第二条语句并没有生效,那么这就涉及到我们怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功,程序如下

 pair::iterator, bool> Insert_Pair;

 Insert_Pair = mapStudent.insert(map::value_type (1, \"student_one\"));

 

我们通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。

 

20、lower_bound 和upper_bound 函数的用法

 

ForwardIter lower_bound(ForwardIter first, ForwardIter last,const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置。

ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)算法返回一个非递减序列[first, last)中第一个大于val的位置。

 

 

 

 

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号