C/C++ 命中率比较高的面试知识点,你都答得上来吗

点击蓝字

3fd720a0c59c705fc67c6bc69054ddd0.png

关注我们

第一部分:计算机基础

1. C/C++内存有哪几种类型?

C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。 

全局变量、static变量会初始化为缺省值,而堆和栈上的变量是随机的,不确定的。

2. 堆和栈的区别?

  • 1).堆存放动态分配的对象——即那些在程序运行时动态分配的对象,比如 new 出来的对象,其生存期由程序控制;

  • 2).栈用来保存定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在;

  • 3).静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;

  • 4).栈和静态内存的对象由编译器自动创建和销毁。

3. 堆和自由存储区的区别?

总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。 

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。

而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。

基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。

4. 程序编译的过程?

程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要经过四个过程:预处理、编译、汇编和链接。具体示例如下。 

一个hello.c的c语言程序如下。

#include <stdio.h>
int main()
{printf("happy new year!\n");return 0;
}

其编译过程如下: 

69e565b848931b56ea26aa5c1423d89b.png
在这里插入图片描述

5. 计算机内部如何存储负数和浮点数?

负数比较容易,就是通过一个标志位和补码来表示。 
拓展问题:

  • 什么是补码? 
    负数补码为反码加1  
    正数补码为原码

  • 负数为什么用补码? 
    统一加减法,正负零问题

对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?

如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。 

无论是单精度还是双精度在存储中都分为三个部分:

6. 函数调用的过程?

如下结构的代码,

int main(void)
{...d = fun(a, b, c);cout<<d<<endl;...return 0;
}

调用fun()的过程大致如下:

  • main()========

  • 1).参数拷贝(压栈),注意顺序是从右到左,即c-b-a;

  • 2).保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位置);

  • 3).跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;

  • fun()=====

  • 4).移动ebp、esp形成新的栈帧结构;

  • 5).压栈(push)形成临时变量并执行相关操作;

  • 6).return一个值;

  • 7).出栈(pop);

  • 8).恢复main函数的栈帧结构;

  • 9).返回main函数;

  • main()========

  • 。。。

7. 左值和右值

不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。

或者左值就是在程序中能够寻址的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。

归纳一下就是:

  • 可以取地址的,有名字的,非临时的就是左值

  • 不能取地址的,没有名字的,临时的,通常生命周期就在某个表达式之内的就是右值

8. 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。

  • 1). 使用的时候要记得指针的长度.

  • 2). malloc的时候得确定在那里free.

  • 3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.

  • 4). 动态分配内存的指针最好不要再次赋值.

  • 5). 在C++中应该优先考虑使用智能指针.

第二部分:C v.s. C++

1. C和C++的区别?

  • 1). C++是C的超集;

  • 2). C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

2. int fun() 和 int fun(void)的区别?

这里考察的是c 中的默认类型机制。

  • 在c中,int fun() 会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制, 而int fun(void)则限制输入类型为一个void。

  • 在c++下,这两种情况都会解读为返回int类型,输入void类型。

3. const 有什么用途

主要有三点:

  • 1).定义只读变量,或者常量(只读变量和常量的区别参考下面一条);

  • 2).修饰函数的参数和函数的返回值;

  • 3).修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数, 可以访问非const成员,但是不能修改;

  • 4).只读对象。只读对象只能调用const成员函数。

class Screen {
public:
const char cha; //const成员变量
char get() const; //const成员函数
};const Screen screen; //只读对象

4. 在C中用const 能定义真正意义上的常量吗?C++中的const呢?

不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的)。

但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。 
补充:

  • 1). c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。

  • 2). 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。

  • 3).c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。

  • 4).c语言中只有enum可以实现真正的常量。

  • 5 ). c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。

  • 6). c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。

下面我们通过代码来看看区别。 
同样一段代码,在c编译器下,打印结果为*pa = 4,a = 4  
在c++编译下打印的结果为 *pa = 4, a = 8

int main(void)
{const int a = 8;int *pa = (int *)&a;*pa = 4;printf("*pa = %d, a = %d", *pa, a);return 0;
}

另外值得一说的是,由于c++中const常量的值在编译期就已经决定,下面的做法是OK的,但是c中是编译通不过的。

int main(void)
{const int a = 8;const int b = 2;int array[a+b] = {0};return 0;
}

5. 宏和内联(inline)函数的比较?

  • 1). 首先宏是C中引入的一种预处理功能;

  • 2). 内联(inline)函数是C++中引入的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;

  • 3). 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;

  • 4). 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;

  • 5). 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译器决定(当然可以通过设置编译器,强制使用内联);

  • 6). 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。

  • 7). 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。 
    更多请参考inline关键字

6. C++中有了malloc / free , 为什么还需要 new / delete?

  • 1). malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

  • 2). 对于非内部数据类型(自定义类型)的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。 
    由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。 
    最后补充一点题外话,new 在申请内存的时候就可以初始化(如下代码), 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。

int *p = new int(1);

特别的,在C++中,如下的代码,用new创建一个对象(new 会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。

#include <cstdlib>
class Test
{...
}Test* pn = new Test;
Test* pm = (Test*)malloc(sizeof(Test));

7. C和C++中的强制类型转换?

C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况;而C++中引入了下面四种转换:

  • 1). static_cast  
    a. 用于基本类型间的转换  
    b. 不能用于基本类型指针间的转换  
    c. 用于有继承关系类对象间的转换和类指针间的转换

  • 2). dynamic_cast  
    a. 用于有继承关系的类指针间的转换  
    b. 用于有交叉关系的类指针间的转换  
    c. 具有类型检查的功能  
    d. 需要虚函数的支持

  • 3). reinterpret_cast  
    a. 用于指针间的类型转换  
    b. 用于整数和指针间的类型转换

  • 4). const_cast  
    a. 用于去掉变量的const属性  
    b. 转换的目标类型必须是指针或者引用

拓展  
在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。

但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。

实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。

class test{int m_value;...
public:operator int()  //类型转换函数{return m_value;}int toInt() //显示调用普通函数来实现类型转换{return m_value}
};int main()
{...test a(5);int i = a;    // 相当于 int i = test::operator int(&a)...return 0;
}

8. static 有什么用途

  • 1). 静态(局部/全局)变量

  • 2). 静态函数

  • 3). 类的静态数据成员

  • 4). 类的静态成员函数

9. 类的静态成员变量和静态成员函数各有哪些特性?

静态成员变量

  • 1). 静态成员变量需要在类内声明(加static),在类外初始化(不能加static),如下例所示;

  • 2). 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量;

  • 3). 可以通过对象名直接访问公有静态成员变量;

  • 4). 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。

class example{
public:
static int m_int; //static成员变量
};int example::m_int = 0; //没有staticcout<<example::m_int; //可以直接通过类名调用静态成员变量

静态成员函数

  • 1). 静态成员函数是类所共享的;

  • 2). 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;

  • 3). 可以通过对象名直接访问公有静态成员函数;

  • 4). 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。

class example{
private:
static int m_int_s; //static成员变量
int m_int;
static int getI() //静态成员函数在普通成员函数前加static即可
{return m_int_s; //如果返回m_int则报错,但是可以return d.m_int是合法的
}
};cout<<example::getI(); //可以直接通过类名调用静态成员变量

10. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:

void foo(int x, int y);

该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

11. 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?

相同点:  
它们的作用是防止头文件被重复包含。 
不同点

  • 1). ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。

  • 2). 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。

  • 3). ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。

  • 4). 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特别需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。

12. 当i是一个整数的时候++i和i++那个更快一点?i++和++i的区别是什么?

答:理论上++i更快,实际与编译器优化有关,通常几乎无差别。

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

i++和++i的考点比较多,简单来说,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,如下使用:

cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;

可以不停的嵌套++i。 
这里有很多的经典笔试题,一起来观摩下:

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

首先是函数的参数入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作:

  • 对于第1个printf,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3;

  • 对于第2个printf,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果

  • 对于第3个printf,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果

  • 对于第4个printf,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。

上面的分析也是基于VS搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。

这里还有一个 i++ 的典型应用案例。

map<char, int> b = {{'a', 1}, {'b', 2}};for(auto iter = b.begin(); iter != b.end();){if(iter->first == 'a'){b.erase(iter++);    // 等价于 auto t = iter; iter = iter + 1; b.erase(t);}else{iter++;}}

第三部分:数组、指针 & 引用

1. 指针和引用的区别?

相同点:

  • 1). 都是地址的概念;

  • 2). 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;

  • 3). 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

不同点:

  • 1). 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;

  • 2). 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;

  • 3). 引用不能为空,指针可以为空;

  • 4). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;

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

  • 6). 引用是类型安全的,而指针不是 (引用比指针多了类型检查)

  • 7). 引用具有更好的可读性和实用性。

2. 引用占用内存空间吗?

如下代码中对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。

比如 Type& name;<===> Type* const name。

int main(void)
{int a = 8;int &b = a;int *p = &b;        // 等价于 int *p = &a;*p = 0;cout<<a; //output 0return 0;
}

3. 三目运算符

在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。

int main(void)
{int a = 8;int b = 6;(a>b ? a : b) = 88;cout<<a; //output 88return 0;
}

4. 指针数组和数组指针的区别

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

  • 数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[n],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。

类型名 (*数组标识符)[数组长度]
  • 指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int *p[n], []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。

类型名 *数组标识符[数组长度]

5. 左值引用与右值引用

该部分主要摘自:c++ 学习笔记

左值引用就是我们通常所说的引用,如下所示。左值引用通常可以看作是变量的别名。

type-id & cast-expression // demo
int a = 10
int &b = aint &c = 10    // 错误,无法对一个立即数做引用const int &d = 10    // 正确, 常引用引用常数量是ok的,其等价于 const int temp = 10; const int &d = temp

右值引用是 C++11 新增的特性,其形式如下所示。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。

type-id && cast-expression  // demo
int &&var = 10;    // okint a = 10
int &&b = a    // 错误, a 为左值int &&c = var    // 错误,var 为左值int &&d = move(a)    // ok, 通过move得到左值的右值引用

在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。

6. 右值引用的意义

  • 右值引用支持移动语义的实现,可以减少拷贝,提升程序的执行效率。

    下面的代码是没有采用右值引用时的实现。

    class Stack
    {
    public:// 构造Stack(int size = 1000) :msize(size), mtop(0){cout << "Stack(int)" << endl;mpstack = new int[size];}// 析构
    ~Stack()
    {cout &lt;&lt; "~Stack()" &lt;&lt; endl;delete[]mpstack;mpstack = nullptr;
    }// 拷贝构造
    Stack(const Stack &amp;src):msize(src.msize), mtop(src.mtop)
    {cout &lt;&lt; "Stack(const Stack&amp;)" &lt;&lt; endl;mpstack = new int[src.msize];for (int i = 0; i &lt; mtop; ++i) {mpstack[i] = src.mpstack[i];}
    }// 赋值重载
    Stack&amp; operator=(const Stack &amp;src)
    {cout &lt;&lt; "operator=" &lt;&lt; endl;if (this == &amp;src)return *this;delete[]mpstack;msize = src.msize;mtop = src.mtop;mpstack = new int[src.msize];for (int i = 0; i &lt; mtop; ++i) {mpstack[i] = src.mpstack[i];}return *this;
    }int getSize() 
    {return msize;
    }
    private:int *mpstack;int mtop;int msize;
    };Stack GetStack(Stack &stack)
    {Stack tmp(stack.getSize());return tmp;
    }int main()
    {Stack s;s = GetStack(s);return 0;
    }

    运行结果如下。

    Stack(int)             // 构造s
    Stack(int)             // 构造tmp
    Stack(const Stack&)    // tmp拷贝构造main函数栈帧上的临时对象
    ~Stack()               // tmp析构
    operator=              // 临时对象赋值给s
    ~Stack()               // 临时对象析构
    ~Stack()               // s析构

    执行代码的过程中调用拷贝构造,将内存中的内容逐个拷贝,在 C++ 11 中可以借助右值引用实现移动拷贝构造和移动赋值来解决这个问题。

    Stack(Stack &&src):msize(src.msize), mtop(src.mtop)
    {cout << "Stack(Stack&&)" << endl;/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;  
    src.mpstack = nullptr;
    }// 带右值引用参数的赋值运算符重载函数
    Stack& operator=(Stack &&src)
    {cout << "operator=(Stack&&)" << endl;if(this == &amp;src)return *this;delete[]mpstack;msize = src.msize;
    mtop = src.mtop;/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;
    src.mpstack = nullptr;return *this;
    }

    执行结果如下。可以看到,在有拷贝构造和移动拷贝构造函数的时候,优先调用了移动拷贝构造和移动赋值。在移动拷贝构造和移动赋值中直接把资源所有权进行了转移,而非拷贝,这就大大提高了执行效率。

    Stack(int)             // 构造s
    Stack(int)             // 构造tmp
    Stack(Stack&&)         // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象
    ~Stack()               // tmp析构
    operator=(Stack&&)     // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s
    ~Stack()               // 临时对象析构
    ~Stack()               // s析构
  • 右值引用可以使重载函数变得更加简洁。右值引用可以适用 const T& 和 T& 形式的参数。

    struct W  
    {  W(int&, int&) {}  
    };  struct X  
    {  X(const int&, int&) {}  
    };  struct Y  
    {  Y(int&, const int&) {}  
    };  struct Z  
    {  Z(const int&, const int&) {}  
    };template <typename T, typename A1, typename A2>  
    T* factory(A1& a1, A2& a2)  
    {  return new T(a1, a2);  
    } template <typename T, typename A1, typename A2>  
    T* factory_new(A1&& a1, A2&& a2)  
    {  return new T(std::forward<A1>(a1), std::forward<A2>(a2));  
    }  // demo
    int a = 2;
    int b = 2;W* c = factory<w>(a, b);    // ok
    Z* d = factory<Z>(2, 2);    // 错误,2 是右值W* pw = factory_new<W>(a, b);    // ok
    X* px = factory_new<X>(2, b);    // ok
    Y* py = factory_new<Y>(a, 2);    // ok
    Z* e = factory_new<Z>(2, 2);    // ok
    W* f = factory_new<W>(2, 2);    // 错误,

    更多相关内容可以参考:c++——左值、右值、左值引用、右值引用

第四部分:C++特性

1. 什么是面向对象(OOP)?面向对象的意义?

Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。 
面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。

2. 解释下封装、继承和多态?

  • 1). 封装: 
    封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。 
    封装的意义在于保护或者防止代码(数据)被我们无意中破坏。 
    从封装的角度看,public, private 和 protected 属性的特点如下。

    • 和 public 一样可以被子类继承

    • 和 private 一样不能在类外被直接调用

    • 特例:在衍生类中可以通过衍生类对象访问,如下代码所示

    • 不管哪种属性,内类都是可以访问的

    • public 是一种暴露的手段,比如暴露接口,类的对象可以访问

    • private 是一种隐藏的手段,类的对象不能访问

    • protected 成员:

class Base  
{  
public:  Base(){};  virtual ~Base(){};  
protected:  int int_pro;  
};
class A : public Base  
{  
public:  A(){};  A(int da){int_pro = da;}  // 通过 obj 对象直接访问 protected 成员void Set(A &obj){obj.int_pro = 24;}    void PrintPro(){cout << "The proteted data is " << int_pro <<endl;}  
};
  • 2). 继承:  继承主要实现重用代码,节省开发时间。  子类可以继承父类的一些东西。

    • a.公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态(基类的私有成员仍然是私有的,不能被这个派生类的子类所访问)。

    • b.私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员(并且不能被这个派生类的子类所访问)。

    • c.保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员(并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的)。

这里特别提一下虚继承。虚继承是解决C++多重继承问题(其一,浪费存储空间;第二,存在二义性问题)的一种手段。比如菱形继承,典型的应用就是 iostream, 其继承于 istream 和 ostream,而 istream 和 ostream 又继承于 ios。

3).多态: 
多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。

3. 什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

  • 1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。

  • 2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。

  • 3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。

更多可以参考下面的代码,比较容易混淆的是赋值操作符,其实区分很简单,在出现等号的时候,如果有构造新的对象时调用的就是构造,不然就是调用赋值操作符。特别注意下面的 b 和 f,一个是拷贝构造,一个是构造。

class A {
public:A() {m = new int[4]{ 1,2,3,4 };std::cout << "constructor" << std::endl;}~A() {if (m != nullptr) {delete[] m;}}A(const A& a) {this->m = new int[4];memcpy(a.m, this->m, this->len * sizeof(int));std::cout << "copy constructor" << std::endl;}// 移动构造A(A&& a) : m(a.m) {a.m = nullptr; std::cout << "move constructor" << std::endl;}// 赋值操作符重载A& operator= (const A& a) {memcpy(a.m, this->m, this->len * sizeof(int));std::cout << "operator" << std::endl;return *this;}private:int len = 4;int* m = nullptr;
};A getA(A a) {return a;
}int main(void)
{A a;    // constructA b = a;    // copy constructA c(a); // copy constructA d;    // constructd = a;  // operateA e = getA(a);  // construct, move constructA f = A();  // constructreturn 0;
}

4. 构造函数和析构函数的执行顺序?

构造函数

  • 1). 首先调用父类的构造函数;

  • 2). 调用成员变量的构造函数;

  • 3). 调用类自身的构造函数。

析构函数

对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。

5. 虚析构函数的作用?

基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。

class A{
public:A(){}//~A(){}virtual ~A(){cout << "A disconstruct" << endl;}  // 虚析构
//   ~A(){cout << "A disconstruct" << endl;}  // 析构};class B : public A{
public:B(){// new memory// ...cout << "B construct" << endl;}~B(){// delete memory// ...cout << "B disconstruct" << endl;}
};int main(int argc, char **argv)
{A *p = new B;// some operations// ...delete p;  // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数return 0;
}

但并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

6. 细看拷贝构造函数

对于 class A,它的拷贝构造函数如下:

A::A(const A &a){}

1) 为什么必须是当前类的引用呢?

循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?

拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能直接转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

7. C++的编译环境

如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。 

25ca9e96eb9f7c8ca7c7b3ee85b2e7f5.png
在这里插入图片描述
#include<iostream>  //C++标准库,不带".h"
#include<string.h>  //C语言兼容库,由编译器厂商提供

值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。

8. Most vexing parse

直接上代码吧。下面 f 和 g 是有问题的,这种情况就称为 Most vexing parse。

class A {
public:A() { cout << "const without param" << endl; }A(int a) { cout << "const with param" << endl; }A(const A& b) { cout << "copy construct" << endl; }
};int main(void)
{A a;    // const(construct) without paramA b(10);    // const with paramA c = A();  // const without paramA d = A(10);    // const with paramA e(d); // copy constructA f();  A g(A());A h{};  // const without paramA i{A{}};   // const without paramreturn 0;
}

问题在哪?

A f();    // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 f,参数无    
A g(A());    // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 g, 参数类型为函数指针,这个函数指针的返回值类型为A,参数无

解决办法参考上面的 h, j。

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

23b39d8b4f6d02951d64ac13440ef523.png

b2b6f9bb77af389e745e671ee01e2f11.gif

戳“阅读原文”我们一起进步

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/337742.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

svn: 没有演进历程信息_使用默认方法的接口演进–第二部分:接口

svn: 没有演进历程信息引入了默认方法以启用接口演进。 如果向后兼容性是不可替代的&#xff0c;则仅限于向接口添加新方法&#xff08;这是它们在JDK中的唯一用法&#xff09;。 但是&#xff0c;如果希望客户端更新其代码&#xff0c;则可以使用默认方法逐步演化接口而不会引…

蓝桥杯7届c语言 c组答案,第七届蓝桥杯C语言C组-(自己懂的题目)

第七届蓝桥杯C语言C组-(自己懂的题目)表示刚刚查了成绩&#xff0c;省赛一等奖&#xff0c;有资格去北京了&#xff0c;然后写一下总结&#xff0c;先来写一下我懂的题目&#xff0c;毕竟我也是菜鸟&#xff0c;听说国赛比预赛难几个等级。。。第一题报纸页数X星球日报和我们地…

关于多线程的几道面试题

点击蓝字关注我们第一题&#xff1a;线程的基本概念、线程的基本状态及状态之间的关系&#xff1f;线程&#xff0c;有时称为轻量级进程&#xff0c;是CPU使用的基本单元&#xff1b;它由线程ID、程序计数器、寄存器集合和堆栈组成。它与属于同一进程的其他线程共享其代码段、数…

大牛谈嵌入式C语言的高级用法

点击蓝字关注我们内存管理我们需要知道——变量&#xff0c;其实是内存地址的一个抽像名字罢了。在静态编译的程序中&#xff0c;所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的&#xff0c;只知道地址。 内存的使用时程序设计中需要考虑的重要因素之一&…

博科光纤交换机java_带有光纤的可扩展,健壮和标准的Java Web服务

博科光纤交换机java这篇博客文章讨论了负载下的基准Web服务性能。 要了解有关Web服务性能理论的更多信息&#xff0c;请阅读利特尔定律&#xff0c;可伸缩性和容错 。 使用阻塞和异步IO对Web服务进行基准测试 Web应用程序&#xff08;或Web服务&#xff09;如何在负载下&#…

很棒的C语言入门笔记,推荐收藏!

点击蓝字关注我们c语言入门C语言一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好&#xff0c;可以用来开发应用软件、驱动、操作系统等。C语言也是其它众多高级语言的鼻祖语言&#xff0c;所以说学习…

C语言的注释要注意几点

点击蓝字关注我们如果领导给你一个项目的源码让你阅读&#xff0c;并理解重构代码&#xff0c;但里面一句注释都没有&#xff0c;我想这肯定是之前同事“删库跑路”了。看一份源码什么很重要&#xff1f;除了各种代码规范之外&#xff0c;还有一个比较重要的就是注释。注释虽然…

java自动推断类型_推断:Facebook的新Java静态分析工具

java自动推断类型如何使用Facebook的Infer改善Java开发工作流程&#xff1f; 如果您与技术话题保持同步&#xff08;如果您正在阅读此博客&#xff0c;我想您会这样做&#xff09;&#xff0c;那么您可能听说过Facebook 刚刚向公众发布的新工具&#xff1a;推断。 由于它来自F…

android官方架构组件,Android 架构组件官方文档01——LifeCycle

使用生命周期感知组件处理生命周期支持生命周期的组件执行操作以响应另一个组件(例如Activity和fragment)的生命周期状态更改。这些组件可帮助您生成组织性更好&#xff0c;并且通常更轻量的代码&#xff0c;这些代码更易于维护。常见的模式是在Activity和fragment的生命周期方…

C语言的核心和灵魂

点击蓝字关注我们提起C语言大部分开发者很自然就会想到指针二字&#xff0c;没错&#xff0c;作为C的核心和灵魂&#xff0c;它的地位咱们就不再赘述了。今天我们想跟大家讲的是指针中的两个特有名词&#xff1a;“悬空指针”和“野指针”。悬空指针C语言中的指针可以指向一块内…

javaone_JavaOne 2015 –提交技巧和建议

javaone大家都知道JavaOne 。 感觉就像一直存在。 而且&#xff0c;即使我们跌宕起伏&#xff0c;而地理位置也不是我们想要的那样&#xff0c;旧金山也很昂贵&#xff0c;而且和。 这是有关各种Java的顶级会议。 今年又再次成为程序委员会&#xff08;“ Java&#xff0c;DevO…

C语言_结构体总结,附实例源码

点击蓝字关注我们当前文章介绍动态堆空间内存分配与释放&#xff0c;C语言结构体定义、初始化、赋值、结构体数组、结构体指针的相关知识点&#xff0c;最后通过一个学生管理系统综合练习结构体数组的使用。1. 动态内存管理C语言代码----->编译----->链接------>可执行…

四大C语言知识总结

点击蓝字关注我们1、#define宏定义以#号开头的都是编译预处理指令&#xff0c;它们不是C语言的成分&#xff0c;但是C程序离不开它们&#xff0c;#define用来定义一个宏&#xff0c;程序在预处理阶段将用define定义的来内容进行了替换。因此在程序运行时&#xff0c;常量表中并…

jdbc select语句_SELECT语句使用JDBC和Hibernate批量获取

jdbc select语句介绍 现在&#xff0c;我已经介绍了Hibernate对INSERT &#xff0c; UPDATE和DELETE语句的批处理支持&#xff0c;是时候分析SELECT语句结果集的批量提取了。 JDBC ResultSet提供了一个客户端Proxy游标&#xff0c;用于获取当前语句的返回数据。 执行该语句后&…

看懂开源项目,你得熟悉这几个 C++11 新特性

点击蓝字关注我们C11 中增加了许多的新特性。在本文中&#xff0c;我们来聊一下 lambda 表达式&#xff0c;闭包&#xff0c;std::function以及std::bind。lambda 表达式C11 中新增了 lambda 表达式这一语言特性。lambda 表达式可以让我们快速和便捷的创建一个 “函数”。下面是…

5元素升级android6,【五元素ifive X.7】无障碍升级,ifveX详细升级固件教程,快为爱机升级吧。...

android系统的乐趣就是可以随意的刷机&#xff0c;所以拿到如此高配置的ifiveX也是想随时体验最新的android系统。而很多朋友可能只会用设备&#xff0c;刷机这种比较技术性的动作就不太会弄了。也就在这周&#xff0c;才帮同事刷RUU把已经无法启动的G12刷了回来。所以刷机还是…

昆仑通态复制的程序可以用吗_昆仑通态专题(七):MCGS组态软件的设备窗口...

点击上方蓝色字体&#xff0c;关注我们设备窗口是MCGS嵌入版组态软件系统的重要组成部分&#xff0c;在设备窗口中建立系统与外部硬件设备的连接关系&#xff0c;使系统能够从外部设备读取数据并控制外部设备的工作状态&#xff0c;实现对工业过程设备的实时监控与操作。01 设备…

嵌入式C语言程序调试和宏使用的技巧

点击蓝字关注我们01.调试相关的宏在Linux使用gcc编译程序的时候&#xff0c;对于调试的语句还具有一些特殊的语法。gcc编译的过程中&#xff0c;会生成一些宏&#xff0c;可以使用这些宏分别打印当前源文件的信息&#xff0c;主要内容是当前的文件、当前运行的函数和当前的程序…

matlab中欠定方程组超定方程组_《数值天气预报》:球坐标系中的基本方程组

人们是如何预报天气的&#xff1f;目前的预报方法主要有两种&#xff1a;一种是基于由各种探测资料绘制的天气图&#xff0c;结合历史资料进行分析预测&#xff1b;另一种是基于大气方程组&#xff0c;利用数值解法对其进行求解&#xff0c;从而得到未来时刻的大气状态。后者就…

浏览器总是跳转到缓存界面_跳转到企业缓存之前要考虑的事项

浏览器总是跳转到缓存界面介绍 关系数据库事务是ACID &#xff0c;强大的一致性模型简化了应用程序开发。 由于启用Hibernate缓存是一项配置 &#xff0c;因此&#xff0c;只要数据访问层开始出现性能问题&#xff0c;就转向缓存非常吸引人。 添加缓存层确实可以提高应用程序性…