欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++面试总结

程序员文章站 2022-03-07 23:19:31
...

原文链接:https://blog.csdn.net/eversliver/article/details/51834399

虚函数与纯虚函数:

引入虚函数是为了动态绑定,引入纯虚函数是为了派生接口 
基类需要虚的析构函数的原因: 
当derived class由一个base class指针被删除而该base class指针为non-virtual的时候,可能会发生内存泄漏,使用虚的析构函数可以解决该问题

++i与i++的区别:

//i++ 实现代码为:
int operator++(int)
{
   int temp = *this;
   ++*this;
   return temp;
}// 返回一个 int 型的对象本身
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
// ++i 实现代码为:
int& operator++()
{
    *this += 1;
    return *this;
}// 返回一个 int 型的对象引用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

++i实际上返回的是一个可进行修改的左值,i++则不是 
下面贴一个典型的题目:

int main()
{
    int i = 1;
    printf("%d,%d\n", ++i, ++i);    //3,3
    printf("%d,%d\n", ++i, i++);    //5,3
    printf("%d,%d\n", i++, i++);    //6,5
    printf("%d,%d\n", i++, ++i);    //8,9
    system("pause");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

上面代码的执行过程如下所示:

首先是函数的入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后在进行的压栈操作: 
对于第5行代码,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3; 
对于第6行代码,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果 
对于第7行代码,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果 
对于第8行代码,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。 
上面的分析也是基于vs搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。

在模板中声明嵌套从属类型的方法:

template里面出现的某个类型如果依赖于某个template参数,那么称之为从属类型,如果从属类型在class中呈嵌套状,那么称之为嵌套从属类型。

template<typename C>
void doSomething(const C & container)
{
    if(container.size() > 0)
        C::iterator iter(container.begin());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面那种情况声明一个iter对象,那么前面加上typename才是合适的,上就是声明嵌套从属类型的方法

auto_ptr可以作为vector的元素吗?:

不可以,因为复制一个auto_ptr的时候,自身的值将会被置为NULL,复制一个auto_ptr相当于改变了这个智能指针的值 
C++标准如是说:“STL元素必须具备拷贝构造和可赋值”,显然autoPtr在向外部拷贝的时候是不具备这个条件的吗所以不可以。

const以及static成员初始化的方法。

static成员一般在类外部初始化,但是const static的整型可以再类声明中进行初始化。static const的其他类型应该载类外进行初始化

如何确保对象在抛出异常时也能被删除?什么是RAII?:

总的思想是使用RAII:应该设计一个class,使其构造函数以及析构函数分别获得以及释放资源 
因为函数的局部对象无论以什么样的方式结束都会调用析构函数,可以将一定要释放的资源放到析构函数中,就可以避免抛出异常的时候资源得以释放 
简单的方法就是使用智能指针 
RAII:resource allocation is initiation

几种继承的作用:

继承描述符 父public成员 父protected成员 父private成员
public 子public成员 子protected成员 通过基类接口访问
protected 子protected成员 子protected成员 通过基类接口访问
private 子private成员 子private成员 通过基类接口访问

- 1、public:只继承基类的接口。当继承是接口的一部分时,就选用public继承。 
- 2、private:只继承基类的实现。当继承是实现细节时,就选用private继承。 
- 3、protected:当继承是面向派生类而不是面向用户接口中的一部分时,就选用protected继承。

关于c++的单例模式:

单例模式的一般形式如下所示,将构造函数、析构函数、复制构造函数、赋值操作符声明为私有,即可实现单例模式

class Singleton{
private:
    static Singleton * _instance;
protected:
    Singleton();
public:
    static Singleton * Instance();
};
Singleton::Singleton(){}
Singleton* Singleton::_instance = nullptr;
Singleton * Singleton::Instance()
{
    if(_instance == nullptr)
        _instance = new Singleton;
    return _instance;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

避免用户复制的行为:可以将拷贝构造函数设置为private或者是C++11中的delete语法 
实现线程安全的单例模式:上面实现中的GetInstance()不是线程安全的,因为在单例的静态初始化中存在竞争条件。如果碰巧有多个线程在同时调用该方法,那么就有可能被构造多次。 
比较简单的做法是在存在竞争条件的地方加上互斥锁。这样做的代价是开销比较高。因为每次方法调用时都需要加锁。 
比较常用的做法是使用双重检查锁定模式(DCLP)。但是DCLP并不能保证在所有编译器和处理器内存模型下都能正常工作。如,共享内存的对称多处理器通常突发式提交内存写操作,这会造成不同线程的写操作重新排序。这种情况通常可以采用volatile解决,他能将读写操作同步到易变数据中,但这样在多线程环境下仍旧存在问题。

c++中相等与等价的区别,哪些容器会使用相等或者是等价?:

相等(equality)是以operator==为基础,如果x==y为真,则判定x和y相等。等价(equavalence)是以operator<为基础,如果!(x < y) && !(y < x)为真,则判定x和y等价。通常,关联容器采用“等价”,而顺序容器采用“相等”。

如何实现仿函数?为什么需要通过继承自unary_function 或者 binary_function来实现仿函数?

function object就是重载了函数调用操作符 operator()的一个struct或者class,所有内置一元仿函数均继承自unary_function,所有内置二元仿函数均继承自binary_function。继承自unary_function和binary_function的仿函数可以成为“可配接“的仿函数。可配接的仿函数,能够与其他STL组件更”和谐“地协同工作。

如果在构造函数和析构函数中抛出异常会发生什么?什么是栈展开?

  • 构造函数抛异常:不会发生资源泄漏。假设在operator new()时抛出异常,那么将会因异常而结束此次调用,内存分配失败,不可能存在内存泄露。假设在别处(operator new() )执行之后抛出异常,此时析构函数调用,已构造的对象将得以正确释放,且自动调用operator delete()释放内存
  • 析构函数抛异常: 
    可以抛出异常,但该异常必须留在析构函数;若析构函数因异常退出,情况会很糟糕(all kinds of bad things are likely to happen) 
    • a、可能使得已分配的对象未能正常析构,造成内存泄露;
    • b、例如在对像数组的析构时,如果对象的析构函数抛出异常,释放代码将引发未定义行为。考虑一个对象数组的中间部分在析构时抛出异常,它无法传播,因为传播的话将使得后续部分不能正常释放;它也无法吸收,因为这违反了”异常中立“原则(异常中立,就是指任何底层的异常都会抛出到上层,也就相当于是异常透明的)。 
      抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部如果是,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。

如何在const成员函数中赋值?

方法是使用mutable去除掉const成员函数的const属性 
其中const_cast与mutable之间是有区别的: 
const_cast: 
强制去掉对象的const属性 
缺点:对const对象,调用包含const_cast的const成员函数,属于未定义行为 
1) 使用场景:对可能要发生变化的成员前,加上存储描述符mutable。 
2) 实质:对加了mutable的成员,无视所有const声明。 
为什么要有这种去除常量标志的需求? 
答:两个概念:物理常量性和逻辑常量性 
物理常量性:实际上就是常量。 
逻辑常量性:对用户而言是常量,但在用户不能访问的细节上不是常量。

两种实现隐式类型转换的方式,以及避免进行隐式类型转换的方法:

  • 1.单参数构造函数或者N参数但是由N-1个默认参数的构造函数
  • 2.使用operator type()来进行向type类型的类型转换
  • 3.避免隐式类型转换的方法,在单参数的构造函数或N个参数中有N-1个是默认参数的构造函数声明之前加上explicit。

STL中的vector:增减元素对迭代器的影响

这个问题主要是针对连续内存容器和非连续内存容器。 
a、对于连续内存容器,如vector、deque等,增减元素均会使得当前之后的所有迭代器失效。因此,以删除元素为例:由于erase()总是指向被删除元素的下一个元素的有效迭代器,因此,可以利用该连续内存容器的成员erase()函数的返回值。常见的编程写法为:

for(auto iter = myvec.begin(); iter != myvec.end())  //另外注意这里用 "!=" 而非 "<"
    {
        if(delete iter)
            iter = myvec.erase(iter);
        else ++iter;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

vector插入元素时位置过于靠前,导致需要后移的元素太多,因此vector增加元素建议使用push_back而非insert; 
(2)、当增加元素后整个vector的大小超过了预设,这时会导致vector重新分分配内存,效率极低。因此习惯的编程方法为:在声明了一个vector后, 
立即调用reserve函数,令vector可以动态扩容。通常vector是按照之前大小的2倍来增长的。 
b、对于非连续内存容器,如set、map等。增减元素只会使得当前迭代器无效。仍以删除元素为例,由于删除元素后,erase()返回的迭代器 
将是无效的迭代器。因此,需要在调用erase()之前,就使得迭代器指向删除元素的下一个元素。常见的编程写法为:

for(auto iter = myset.begin(); iter != myset.end())  //另外注意这里用 "!=" 而非 "<"
{
    if(delete iter)
        myset.erase(iter++);  //**使用一个后置自增就OK了**
    else ++iter;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其实在C++11中erase的返回值就是下一个节点,也可以利用函数的返回值

new和malloc的区别

下面首先对new进行一些说明: 
new可分为operator new(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new执行和malloc相同的任务,即分配内存,但对构造函数一无所知;而new operator则调用operator new,分配内存后再调用对象构造函数进行对象的构造。

其中operator new是可以重载的。placement new,就是operator new的一个重载版本(注意operator new所做的仅仅只是申请一块内存,除此之外并不去做任何的操作),允许你在一个已经分配好的内存中构造一个新的对象。而网上对new说法,大多针对operator new而言,因此说new是带有类型的(以为调用了类的构造函数),不过如果直接就说new是带有类型的话,明显是不合适的,比如原生的operator new。下面这个程序是用代理模式实现一个自定义二维数组,在第二个维度拷贝构造的时候, 拷贝构造需要深拷贝(当然第一个维度也需要),执行深拷贝时代码大致如下: 
class Array2D //二维数组模板 

private: 
size_t length2,length1; //数组各个维的大小 
Array1D* data; 

void* raw =::operator new%29”>
data = static_cast

有关Copy Constructor

这是很复杂的一种情况,是关于类的copy constructor的。首先先介绍一些概念。 
同default constructor一样,标准保证,如果类作者没有为class声明一个copy constructor,那么编译器会在需要的时候产生出来(这也是一个常考点:问道”如果类作者未定义出default/copy constructor,编译器会自动产生一个吗?”答案是否定的) 
不过请注意!!这里编译器即使产生出来,也是为满足它的需求,而非类作者的需求! 
而什么时候是编译器”需要”的时候呢?是在当这个class 【不表现出】bitwise copy semantics(位逐次拷贝,即浅拷贝)的时候。 
在4中情况下class【不表现出】bitwise copy semantics 
(1)、当class内含一个member object且该member object声明了一个copy constructor(无论该copy ctor是类作者自己生明的还是编译器合成的); 
(2)、当class继承自一个base class且该base class有一个copy constructor(无论该copy ctor是类作者自己生明的还是编译器合成的) 
(3)、当class声明了virtual function; 
(4)、当class派生自一个继承链,且该链中存在virtual base class时。 
言归正传,如果class中仅仅是一些普通资源,那么default memberwise copy是完全够用的;然而,在该class中存在了一块动态分配的内存,并且在之后执行了bitwise copy semantics后,将会有一个按位拷贝的对象和原来class中的某个成员指向同一块heap空间,当执行它们的析构函数后,该内存将被释放两次,这是未定义的行为。因此,在必要的时候需要使用user-defined explicit copy constructor,来避免内存泄露

STL中排序算法的实现:

STL中的sort(),在数据量大时,采用quicksort,分段递归排序;一旦分段后的数量小于某个门限值,改用Insertion sort,避免quicksort深度递归带来的过大的额外负担,如果递归层次过深,还会改用heapsort。

指针和引用的区别:

本质:指针是一个变量,存储内容是一个地址,指向内存的一个存储单元。而引用是原变量的一个别名,实质上和原变量是一个东西,是某块内存的别名。 
指针的值可以为空,且非const指针可以被重新赋值以指向另一个不同的对象。而引用的值不能为空,并且引用在定义的时候必须初始化,一旦初始化,就和原变量“绑定”,不能更改这个绑定关系。 
不过如下的写法也是同的过编译器的:

int *iptr = NULL;
int& iref = *iptr;
  • 1
  • 2

对指针执行sizeof()操作得到的是指针本身的大小(32位系统为4,64位系统为8)。而对引用执行sizeof()操作,由于引用本身只是一个被引用的别名,所以得到的是所绑定的对象的所占内存大小。 
指针的自增(++)运算表示对地址的自增,自增大小要看所指向单元的类型。而引用的自增(++)运算表示对值的自增。 
在作为函数参数进行传递时的区别:指针作为函数传输作为传递时,函数内部的指针形参是指针实参的一个副本,改变指针形参并不能改变指针实参的值,通过解引用*运算符来更改指针所指向的内存单元里的数据。而引用在作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

关于shared_ptr的要点:

总结下来需要注意的大概有下面几点: 
1)、尽量避免使用raw pointer构建shared_ptr 
2)、shared_ptr使得依据共享生命周期而经行地资源管理进行垃圾回收更为方便 
3)、shared_ptr对象的大小通常是unique_ptr的两倍,这个差异是由于Control Block导致的,并且shared_ptr的引用计数的操作是原子的 
4)、默认的资源销毁是采用delete,但是shared_ptr也支持用户提供deleter,与unique_ptr不同,不同类型的deleter对shared_ptr的类型没有影响。

c与c++的区别

1、C++是面向对象语言,C是面向过程语言。 
2、结构:C以结构体struct为核心结构;C++以类class为核心结构。 
3、多态:C可以以宏定义的方式“自定义”部分地支持多态;C++自身提供多态,并以模板templates支持编译期多态,以虚函数virtual function支持运行期多态。 
4、头文件的调用:C++用< >代替” “代表系统头文件;且复用C的头文件时,去掉”.h”在开头加上”C”。 
5、输入输出:鉴于C++中以对象作为核心,输入和输出都是在流对象上的操作。 
6、封装:C中的封装由于struct的特性全部为公有封装,C++中的封装由于class的特性更加完善、安全。 
7、常见风格:C中常用宏定义来进行文本替换,不具有类型安全性;C++中常建议采用常量定义,具有类型安全性。 
8、效率:常见的说法是同等目的C通常比C++更富有效率(这其实有一定的误解,主要在于C++代码更难于优化且少有人使用编译期求值的特性)。 
9、常用语言/库特性: 
a、数组:C中采用内建数组,C++中建议采用vector。相比之下vector的大小可以动态增长,且使用一些技巧后增长并不低效,且成员函数丰富。 
b、字符串 C中采用C风格的string(实则为字符串数组),C++中建议采用string,对比与上一条类似。 
c、内存分配:C中使用malloc与free,它们是是C标准库函数,C++中建议使用new/delete代替前者,他们说是C++的运算符(这是笔试面试常考点)以C++中的new为例,new可分为operator new(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new执行和malloc相同的任务,即分配内存,但对构造函数一无所知;而 new operator则调用operator new,分配内存后再调用对象构造函数进行对象的构造。其中operator new是可以重载的。placement new,就是operator new的一个重载版本,允许你在一个已经分配好的内存中构造一个新的对象。 
d、指针:C中通常使用的是原生指针(raw pointer),由于常出现程序员在申请后忘记释放造成资源泄漏的问题,在C++98中加入了“第一代”基于引用计数的智能指针auto_ptr,由于初代的各种问题(主要是无法解决循环指针),在03标准也就是TR1中引入了shared_ptr,weak_ptr和unique_ptr这三个功能各异的智能指针,并与11标准中正式确定,较好的解决了上述问题。 
仅有C++才有的常用特性: 
1、语言(范式)特性: 
a、面向对象编程:C++中以关键字class和多态特性支持的一种编程范式; 
b、泛型编程:C++中以关键字template支持的一种编程范式; 
c、模板元编程 :C++中以模板特化和模板递归调用机制支持的一种编程范式。 
d、C++中以对象和类型作为整个程序的核心,在对象方面,时刻注意对象创建和析构的成本,例如有一个很常用的(具名)返回值优化((N)RVO); 
在类型方面,有运行时类型信息(RTTI)等技术作为C++类型技术的支撑。 
e、函数重载:C++允许拥有不同变量但具有相同函数名的函数(函数重载的编译器实现方式、函数重载和(主)模板特化的区别都曾考过)。 
f、异常:以catch、throw、try等关键字支持的一种机制。 
g、名字空间:namespace,可以避免和减少命名冲突且让代码具有更强的可读性。 
h、谓词用法:通常以bool函数或仿函数(functor)或lambda函数的形式,出现在STL的大多数算法的第三个元素。 
2、常见关键字(操作符)特性: 
a、auto:在C中,auto代表自动类型通常都可省略;而在C++11新标准中,则起到一种“动态类型”的作用——通常在自动类型推导和decltype搭配使用。 
b、空指针:在C中常以NULL代表空指针,在C++中根据新标准用nullptr来代表空指针。 
c、&: 在C中仅代表取某个左值(lvalue)的地址,在C++中还可以表示引用(别名)。 
d、&&:在C中仅能表示逻辑与,在C++中还可以表示右值引用。 
e、[]:在C中仅能表示下标操作符,在C++中还可以表示lambda函数的捕捉列表。 
f、{}:在C中仅能用于数组的初始化,在C++中由于引入了初始化列表(initializer_list),可用于任何类型、容器等的初始化。 
g、常量定义:C中常以define来定义常量,C++中用const来定义运行期常量,用constexpr来定义编译器常量。 
3、常用新特性: 
a、右值引用和move语义(太多内容,建议自查)。 
b、基于范围的for循环(与python中的写法类似,常用于容器)。 
c、基于auto——decltype的自动类型推导。 
d、lambda函数(一种局部、匿名函数,高效方便地出现在需要局部、匿名语义的地方)。 
e、标准规范后的多线程库。

c++中的转换机制,各适合什么样的环境,以及dynamic_cast转换失败的时候会出现什么样的情况

dynamic_cast失败的时候,对指针会返回NULL,对引用,会跑出bad_cast异常 
还有就是c++中几种新的cast类型: 
然后看一下各自的适用范围: 
- static_cast:static_cast基本上拥有与C旧式转型相同的威力和意义,以及相同的限制。但是,该类型转换操作符不能移除常量性,因为有一个专门的操作符用来移除常量性。 
- const_cast:用来改变表达式中的常量性(constness)或者易变形(volatileness),只能用于此功能。 
- dynamic_cast:将指向基类basic class object的pointer或者reference转型为指向派生类derived(或这sibling base)class object的pointer或者reference中,并且可以获知是否转型成功:如果转型失败,当转型对象是指针的时候会返回一个null指针;当转型对象是reference会抛出一个异常exception。dynamic_cast无法应用在缺乏虚函数的类型上,也不能改变类型的常量性。 
此外,dynamic_cast还有一个用途就是找出被对象占用的内存的起始点。 
- reinterpret_cast:这个操作符的转换结果几乎总是和编译器平台相关,所以不具有移植性。reinterpret_cast的最常用用途是转换“函数指针”类型:

typedef void(*FuncPtr)();
int doSomething();
int main()
{
    FuncPtr funcPtrArray[10];
    funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

构造函数中是否可以调用虚函数,为什么?语法上可以通过吗?语意上呢?

语法上可以,但是语意上不可以 
derived class对象内的base class成分会在derived class自身构造之前构造完毕。因此,在base class的构造函数中执行的virtual函数将会是base class的版本,决不会是derived class的版本.即使目前确实正在构造derived class。

深拷贝和浅拷贝的区别

浅拷贝:如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会根据需要生成一个默认的拷贝构造函数,完成对象之间的位拷贝。default memberwise copy即称为浅拷贝。 
此处需要注意,并非像大多数人认为的“如果class未定义出copy constructor,那么编译器就会为之合成一个执行default memberwise copy语义的copy constructor”。 
通常情况下,只有在default copy constructor被视为trivial时,才会发生上述情况。一个class,如果既没有任何base/member class含有copy constructor,也没有任何virtual base class或 virtual functions,它就会被视为trivial。通常情况下,浅拷贝是够用的。 
深拷贝:然而在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。如果此时B中执行析构函数释放掉指向那一块堆的指针,这时A内的指针就将成为悬挂指针。 因此,这种情况下不能简单地复制指针,而应该复制“资源”,也就是再重新开辟一块同样大小的内存空间。

动态绑定和静态绑定的区别

  • (1)、对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
  • (2)、对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
  • (3)、静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
  • (4)、动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

关于模板重载决议的相关问题:

template<typename T> void f(T);/* a */         
template<typename T> void f(T*);/* b */        
template< > void f<int>(int*);/* c */          
int* p;                                        
f(p);
  • 1
  • 2
  • 3
  • 4
  • 5
template<typename T> void f(T);/* a */
template< > void f<int*>(int*);/* b */
template<typename T> void f(T*);/* c */
int* p;
f(p);
  • 1
  • 2
  • 3
  • 4
  • 5

分析前先回顾一下模板特化的东西。 
(1)、非特化的模板也被称为主模板; 
(2)、类模板能全特化和偏特化; 
(3)、函数模板只能全特化,不过由于函数重载的原因,能达到偏特化的效果。 
对于1来说: 
a是第一个主模板,b是第二个主模板,且b是第一个主模板a的重载而非偏特化(函数模板没有偏特化)。c是第二个主模板b的显式特化(全特化)。 
在f(p)调用时,发生重载决议,会无视特化存在(标准规定:重载决议无视模板特化,重载决议只会发生在主模板之间)。在主模板a和b中决议出b,即第二个主模板被决议选中,然后再调用其全特化版本c。 
而对与2来说: 
这里a是第一个主模板,b是第一个主模板a的全特化,c是第二个主模板。在f(p)调用时,发生重载决议,同样会无视特化存在,在主模板a和c中决议出c,而c并无全特化版本,因此直接调用c。

virtual函数能声明为内联吗?为什么?

不能。原因:inline是编译期决定,他意味着在执行前就将调用动作替换为被调用函数的本体;virtual是运行期决定,他意味着直道运行期才决定调用哪个函数。 
这两者之间通常是冲突的。然而也有特例,就是当编译阶段就已经知道调用虚函数的指针为多态指针。这里就不再敖述了。

哪些类型的对象不可以作为union的成员?为什么有这种限制?

标准规定,凡是具有non-trivial constructor、non-trivial destructor、non-trivial copy constructor、non-trivial assignment operator的class对象都不能作为union的成员。 
即是说,这个class的以上四种成员必须均经由编译器合成且该class无虚函数和虚基类。 有这种限制是为了兼容C。

C++11有哪些进步?有哪些新的东西?

  • lambda
  • 线程库
  • 智能指针
  • auto

如何实现一个不能在堆分配的类,如何实现一个不能被继承的类

如何实现一个不能在堆上分配的类,如果要在堆上分配就是会使用new,所以可以重载new 操作符,并将其重载于class A的private内:

class A
{
public:
    A(int a):_x(a){}
    int Display() {
        return _x;
    }
    void setVal(int x) {
        _x = x;
        return;
    }
private:
    //
    int _x;
    void* operator new(size_t t){
    }
};
//如何实现一个不能被继承的类,这里有一个比较简单的方法,利用C++11的新关键字final:
class B final {
public:
    B(int a) {
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

关于智能指针:

这里要提及的首先有三点: 
shared_ptr是原始指针大小的两倍。 
引用计数的内存必须被动态分配 
引用计数的改变(increments and decrements)必须是原子的

#include <memory>
#include <iostream>
using namespace std;
int main()
{
    int *rpw = new int(12);
    {
        shared_ptr<int> isptr1(rpw);
        cout << sizeof(rpw) << endl;
        cout << sizeof(isptr1) << endl;
    }
    system("pause");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

明显看到验证了第一条。为什么会是两倍具体原因后面会分析与unique_ptr类似,shared_ptr使用delete作为默认的资源析构函数,但是也可以使用用户自己提供的删除函数(deleter)。不过与unique_ptr不同的是,(unique_ptr的deleter是unique_ptr的类型的一部分)shared_ptr的deleter的类型不再是shared_ptr的类型的一部分。如下示例:

    auto del1 = [](int *p) {
        cout << "del1" << endl;
        delete p;
    };
    auto del2 = [](int *p) {
        cout << "del2" << endl;
        delete p;
    };
    shared_ptr<int> isptr1(new int(12), del1);
    shared_ptr<int> isptr2(new int(10), del2);
    vector<shared_ptr<int>> vsptr{ isptr1,isptr2 };
//这里的isptr1和isptr2的类型是一样的,而对于unique_ptr则不同:
    auto del1 = [](int *p) {
        cout << "del1" << endl;
        delete p;
    };
    auto del2 = [](int *p) {
        cout << "del2" << endl;
        delete p;
    };
    unique_ptr<int, decltype(del1)> iuptr1(new int(12), del1);
    unique_ptr<int, decltype(del2)> iuptr2(new int(10), del2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里的iuptr1和iuptr2是两种不同的类型。 
回到上面的分析中,为什么shared_ptr的大小是raw pointer的两倍的呢?原因主要是在shared_ptr的内部不只有一个类似原始指针的指向object的指针,还有一个指向Control Block的指针:

如上所示,清晰的看到shared_ptr的sizeof返回值应该是2个指针的大小,其中一个指针指向需要指向的object,另外一个指针指向Control Block。Control Block中包含了对这个shared_ptr控制所必需的一些信息,包括引用计数Reference Count、Weak Count、以及在Other Data中会存放用户指定的deleter函数,分配器(allocator)等。所以从shared_ptr的开销角度来说,接下主要是讨论Control Block的创建已经创建带来的问题和Control Block的开销。 
一般来说在三种情况下会创建Control Block 
通过raw pointer创建shared_ptr的时候 
通过make_shared创建shared_ptr的时候 
通过unique_ptr转化创建shared_ptr的时候 
上面三种情况都会创建Control Block,但是问题就出在这个Control Block上,稍后讨论该部分的开销。如果使用同一个raw pointer创建shared_ptr就会出现两个不同的shared_ptr指向同一个raw pointer指向的资源,但是有两个不同的Control Block,当一个的引用计数为0的时候,就会调用deleter释放该资源,那么当另一个shared_ptr也要释放该资源的时候就会发生释放已经被释放的资源的错误,如下所示:

    int *rpw = new int(12);
    {
        shared_ptr<int> isptr1(rpw);
        shared_ptr<int> isptr2(rpw);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

显然这种错误是致命的,因为有可能是发生在析构函数中,接下来还会导致资源泄漏,原本我们是为了防止资源泄漏的。所以,这里建议尽我们在使用shared_ptr的时候应当避免使用raw pointer创建shared_ptr,也就是尽量避免。

虚基类:被虚继承之后的基类类叫做虚基类,而含有纯虚函数的类叫做抽象类

关于C++中的运算符:

using namespace std;

int main()
{
    unsigned char a = 0x45;
    unsigned char b = ~a >> 4 + 1;
    printf("b = %d\n", b);
    system("pause");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

答案是250。这里按照常理来说答案实际上应该是2,也就是运算符顺序是:(~a)>>(4 + 1);但是还有一点应该注意的是:调试的过程中进入汇编指令。 可以看到高级语句转换为汇编语言以后,是先执行取反再位移的。 我们看到**eax是16位的寄存器,于是在机器中 
0xA5的寄存中表达是0000 0000 1010 0101,取反是1111 1111 0101 1010,那么右移5位是 
0000 0111 1111 1010,**由于是unsigned char型的只能表示低8位的数值,即250。 
下图表明了c++中的运算符优先级:

使用表达式来判断一个数是不是2的n次方:

!(x^(x-1))
  • 1

下面代码的分析

int f(int x, int y)
{
    return (x&y)+((x^y)>>1);
}
  • 1
  • 2
  • 3
  • 4

求值f(729, 271)=500 
简单的方法来看这个问题就是,x&y是取相同的位与,这个的结果是x和y相同位的一半,x^y是取x和y的不同位,右移相当于除以2,所以这个函数的功能是取两个数的平均值。(729+271)/2=500;

不使用if,switch以及其他的判断语句,找到两个数中最大者:

  • 方法1:
int max = ((a+b)+abs(a-b))/2;
  • 1
  • 方法2:
int c = a - b;
c = (unsigned)(c)>>(sizeof(int) * 8 - 1);
  • 1
  • 2

交换a,b的值同时不使用任何的中间变量:

void swap(int & a, int & b)
{
    a = a ^ b;
    b = a ^ b;//注意1.
    a = a ^ b;//注意2.
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1处:b = a ^ b ^ b; 所以这里就是a 
2处:a = a ^ b ^ a; 所以这里就是b 
上面这样做的好处是,不会出现溢出的问题

extern的作用

C++语言支持函数重载,C语言不支持函数重载。 函数被C++编译后在库中的名字与C语言的不同。 假设某个函数的原型为void foo(int x, int y)。 该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。C++提供了C连接交换指定符号extern “C”解决名字匹配问题,这样就可以正确的链接在库文件中已经编译好了的c函数

使用宏来来表示一年中有多少秒:

主要需要考虑的问题是对于类似16位机的溢出问题,所以可能需要加上UL限定符,让编译器可以识别

#define SECONDS_PER_YEAR (60*60*24*365)UL
  • 1

在const函数中修改类成员的方法:

将类成员的修饰符加上mutable就可以了。

小结一下sizeof和strlen之间的区别:

  • (1)sizeof操作符的结果类型是size_t,它在头文件中的typedef为unsigned int类型。 该类型保证能容纳实现所建立的最大对象的字节大小。
  • (2)sizeof是运算符,strlen是函数。
  • (3)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。
  • (4)数组做sizeof的参数不退化,传递给strlen就退化为指针。
  • (5)大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。
  • (6)strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型占内存的大小。
  • (7)sizeof后如果是类型必须加括号,如果是变量名可以不加括号。 这是因为sizeof是个操作符而不是个函数。
  • (8)当使用了一个结构类型或变量时,sizeof返回实际的大小。 当使用一静态的空间数组时,sizeof返回全部数组的尺寸。 sizeof操作符不能返回被动态分配的数组或外部的数组的尺寸。
  • (9)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如fun(char [8])、 fun(char [])都等价于fun(char *)。 在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。 如果想在函数内知道数组的大小,需要这样做:进入函数后用memcpy将数组复制出来,长度由另一个形参传进去。
  • (10)计算结构变量的大小就必须讨论数据对齐问题。 为了使CPU存取的速度最快(这同CPU取数操作有关,详细的介绍可以参考一些计算机原理方面的书),C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但在理论上CPU速度快了。 当然,这样的设置会在读写一些别的应用程序生成的数据文件或交换数据时带来不便。 MS VC++中的对齐设定,有时候sizeof得到的与实际不等。 一般在VC++中加上#pragma pack(n)的设定即可。 或者如果要按字节存储,而不进行数据对齐,可以在Options对话框中修改Advanced Compiler选项卡中的“DataAlignment”为按字节对齐。
  • (11)sizeof操作符不能用于函数类型,不完全类型或位字段。不完全类型指具有未知存储大小数据的数据类型,如未知存储大小的数组类型、 未知内容的结构或联合类型、 void类型等。 
    *还有如下的一些结论: 
    (1)unsigned影响的只是最高位bit的意义(正/负),数据长度是不会被改变的 
    (2)自定义类型的sizeof取值等同于它的类型原形。 
    (3)对函数使用sizeof,在编译阶段会被函数返回值的类型取代。 如:int f1(){return 0;},sizeof(f1())得到的值是4 
    (4)只要是指针,大小就是4。 
    (5)数组的大小是各维数的乘积×数组元素的大小。 
    指的注意的一点是,float和long实际上都是占4个字节 
    要明确sizeof不是函数,也不是一元运算符,它是个类似宏定义的特殊关键字,sizeof()。 括号内的内容在编译过程中是不被编译的,而是被替代类型,如int a=8;sizeof(a)。 在编译过程中,不管a的值是什么,只是被替换成类型sizeof(int),结果为4。如果sizeof(a=6)呢?也是一样地转换成a的类型。 但是要注意,因为a=6是不被编译的,所以执行完sizeof(a=6)后,a的值还是8,是不变的

内联函数与宏之间的差别:

内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。 而宏只是一个简单的替换。**内联函数要做参数类型检查,这是内联函数跟宏相比的优势。**inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。 至于是否需要inline函数,就需要根据实际情况来取舍了。

inline一般用于下面这样的情况: 
- (1)一个函数不断被重复调用。 
- (2)函数只有简单的几行,且函数内不包含for、 while、 switch语句。

指针与引用之间的区别

  • (1)非空区别。 在任何情况下都不能使用指向空值的引用。 一个引用必须总是指向某些对象。 因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。 相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。
  • (2)合法性区别。 在使用引用之前不需要测试它的合法性。 相反,指针则应该总是被测试,防止其为空。
  • (3)可修改区别。 指针与引用的另一个重要的区别是指针可以被重新赋值以指向另一个不同的对象。 但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
  • (4)应用区别。 总的来说,在以下情况下应该使用指针:一是考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空),二是需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。 如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么应该使用引用。

下面的代码的问题以及修改方式?

char * strA()
{
    char str[] = "hello world!";
}
  • 1
  • 2
  • 3
  • 4
  • 上面的代码主要错在char str[]在栈中声明了一个局部的数组,这样函数退出的时候栈帧已经释放了,这样是不正确的。 
    一个改法是:
const char * strA()
{
    char * str = "hello world!";
}
  • 1
  • 2
  • 3
  • 4

这里要明确的一点是,char * 与char []之间的区别:

char str[] = “hello world” 实际上分配了一个局部数组(这样由于内存被释放了,会打印出来乱码) 
char * str = “hello world”实际上分配了一个全局数组(因为这里的str不是分配在栈上的,打印出来的值是正常的,字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区))

上面的还有一种改法就是:

const char * strA()
{
    static char str[] = "hello world";
    return str;
}
  • 1
  • 2
  • 3
  • 4
  • 5

下面这道题的打印结果是什么:

class A{
public:
    int _a;
    A(){
        _a = 1;
    }
    void print(){
        cout << _a << endl;
    }
};


class B : public A{
public:
    int _a;
    B(){
        _a = 2;
    }
};

int main()
{
    B b;
    b.print();
    cout << b._a << endl;
    system("pause");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 答案是会打印出1, 2.这里首先应该注意一下A与B的内存模型,即是域相同,子类中的域是不会覆盖父类中的域的。这里就是说_a这个变量在A和B中各有一个。 
    所以这里b.print打印出来的实际上是1,而b._a打印的是2; 
    同样,这里如果重载了print函数的话那么b.print()打印出来的就是2了;

下面描述正确的是:

  • A.函数的形参在函数未调用时预分配存储空间
  • B.若函数的定义出现在主函数之前,则可以不必再说明
  • C.若一个函数没有return语句,则什么值都不返回
  • D.一般来说,函数的形参和实参的类型应该一致

A:错误的,调用到实参才会分配空间。 
B:函数需要在它被调用之前被声明,这个跟main()函数无关。 
C:错误的,在主函数main中可以不写return语句,因为编译器会隐式返回0;但是在一 
般函数中没return语句是不行的。 
D:正确的。