C++面试题目

C++和C的区别

总览

  1. C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。
  2. C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。 所以C与C++的最大区别在于它们的用于解决问题的思想方法不一样。之所以说C++比C更先进,是因为“ 设计这个概念已经被融入到C++之中 ”。

设计思想上:

  • C++是面向对象的语言,而C是面向过程的结构化编程语言

语法上:

  • C++具有封装、继承和多态三种特性。封装体现在类,继承体现在类,多态体现在虚函数、类、友元
  • C++相比C,增加多许多类型安全的功能,比如强制类型转换(四个),const、private、智能指针、引用
  • C++支持范式编程,比如模板类、函数模板等,STL

C++对C的增强

  • (1) 类型检查更为严格:C++ 通过使用基类指针或引用来代替 void* 的使用,避免了这个问题(其实也是体现了类继承的多态性)
  • (2) 增加了面向对象的机制:C++允许结构体中封装函数,而在其他的地方直接调用这个函数。这个封装好的可直接调用的模块有个新名词——对象;并且也把结构体换一个名字——类。这就是面向对象的思想。在构建对象的时候,把对象的一些操作全部定义好并且给出接口的方式,对于外部使用者而言,可以不需要知道函数的处理过程,只需要知道调用方式、传递参数、返回值、处理结果。
  • (3)增加了泛型编程的机制(Template):不同的类型采用相同的方式来操作,模版技术具有比类、函数更高的抽象水平,因为模版能够生成出(实例化)类和函数。可以用来: 1,替换类型(最常用的 vector<T>);2,判定类型(is_integral<T>)和类型间的关系(is_convertible<From, To>) ;3,控制模版函数的实例化(SFINAE ---> enable_if<bool, T>)
  • (4)增加了异常处理:C++ 提供了一系列标准的异常,定义在 <exception> 中,可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

  • (5)增加了运算符重载:C++ 可以实现函数重载,条件是:函数名必须相同,返回值类型也必须相同,但参数的个数、类型或顺序至少有其一不同。而重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。大多数的重载运算符可被定义为普通的非成员函数(func(a, b) 形式调用)或者被定义为类成员函数(a.func(b) 形式调用)
  • (6)增加了标准模板库(STL)STL 的数据结构和内部实现
  • (7)C和C++动态管理内存的方法不一样:C是使用malloc、free函数,而C++不仅有malloc/free,还有new/delete关键字。那malloc/free和new/delete差别? malloc/free和new/delete差别: ①、malloc/free是C和C++语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。 ②、由于malloc/free是库函数不是运算符,不在编译器范围之内,不能够把执行构造函数和析构函数的任务强加入malloc/free。因此C++需要一个能完成动态内存分配和初始化工作的运算符new,一个能完成清理与释放内存工作的运算符delete。 ③、new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。 ④、malloc是从堆上开辟空间,而new是从自由存储区开辟(自由存储区是从C++抽象出来的概念,不仅可以是堆,还可以是静态存储区)。 ⑤、malloc对开辟的空间大小有严格指定,而new只需要对象名。 ⑥、malloc开辟的内存如果太小,想要换一块大一点的,可以调用relloc实现,但是new没有直观的方法来改变。
  • (8)C++有很多特有的输入输出流
  • (9)C++中有引用,而C没有。 那指针和引用有什么差别? 指针和引用的区别: ①、指针有自己的一块空间,而引用只是一个别名。 ②、使用sizeof查看一个指针大小为4(32位),而引用的大小是被引用对象的大小。 ③、指针可以是NULL,而引用必须被初始化且必须是对一个以初始化对象的引用。 ④、作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象。 ⑤、指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被修改。 ⑥、指针可以有多级指针(**p),而引用只有一级。 ⑦、指针和引用使用++运算符的意义不一样。

参考链接

请说一下C/C++ 中指针和引用的区别?

  • 1.指针有自己的一块空间,而引用只是一个别名;
  • 2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小
  • 3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
  • 4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象
  • 5.可以有const指针,但是没有const引用
  • 6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  • 7.指针可以有多级指针(**p),而引用至于一级;
  • 8.指针和引用使用++运算符的意义不一样;
  • 9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

怎么判断一个数是二的倍数,怎么求一个数中有几个1,说一下你的思路并手写代码

  • 判断一个数是不是二的倍数,即判断该数二进制末位是不是0:
  • a % 2 == 0 或者a & 0x0001 == 0。
  • 2、求一个数中1的位数,可以直接逐位除十取余判断:
#include <iostream>
#include <memory>

int judgment_function(size_t &number){
    int count = 0;
    while (number){
        if (number % 10 == 1){
            count++;
        }
        number /= 10;
    }
    return count;
}

int main()
{
    size_t a = 11123422234411111111;
    std::cout << judgment_function(a) << std::endl;
}

数组和指针的区别

区别

  • 赋值:同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
  • 存储方式:数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的,可以随机访问。指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
  • 求sizeof:数组所占存储空间的内存:sizeof(数组名),数组的大小 =  sizeof(数组名)/sizeof(数据类型)。在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。

初始化方式不同

  • 传参方式:数组传参时,会退化为指针,C语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。一级指针传参可以接受的参数类型:(1)可以是一个整形指针 (2)可以是整型变量地址 (3)可以是一维整型数组数组名;
  • 当函数参数部分是二级指针时,可以接受的参数类型:(1)二级指针变量(2)一级指针变量地址(3)一维指针数组的数组名

野指针

  • “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。野指针的成因主要有两种:
  • 一、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
  • 二、指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。

C++面经 基本语言(二)

请你回答一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数 考点:虚函数 析构函数

  • 将可能会被继承的父类的析构函数设置为虚函数,可以保证new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
  • C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
  • 如果基类的构造函数不使用虚函数 virtual,其派生的子类都会使用父类的构造方法,不会使用自己重新定义构造函数
  • 详见  C++ 查漏补缺    使用 “多态 虚函数理论” 作为索引

请你来说一下函数指针

定义

  • 函数指针是指向函数的指针变量
  • 函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数
  • C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

用途

  • 调用函数和做函数的参数,比如回调函数。

示例

  • char * fun(char * p)  {…}       // 函数fun
  • char * (*pf)(char * p);             // 函数指针pf
  • pf = fun;                        // 函数指针pf指向函数fun
  • pf(p);                        // 通过函数指针pf调用函数fun
#include <iostream>

int max(int x,int y){
    return x > y ? x : y;
}

int main(void)
{
    //p是函数指针
    //typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
    int (*p)(int,int) = &max; //&可以省略
    int a = 2,b=3,c=4;
    /* 与直接调用函数等价,d = max(max(a, b), c) */
    int d = p(p(a,b),c);
    std::cout << "最大的数字是:"  << d << std::endl;
}

参考链接

回调函数

函数指针作为某个函数的参数

  • 函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
  • 简单讲:回调函数是由别人的函数执行时调用你实现的函数

以下是来自知乎作者常溪玲的解说:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

实例

  • 实例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。
  • 实例中我们定义了回调函数 getNextRandomValue,它返回一个随机值,它作为一个函数指针传递给 populate_array 函数。
  • populate_array 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。
#include <iostream>

//回调函数
void populate_array(int *array,size_t array_size,int(*get_next_value)(void)){
    for (auto i = 0; i < array_size; ++i) {
        array[i] = get_next_value();
    }
}

//获取随机数值
int get_next_value(void){
    return rand();
}

int main(void)
{
    int array[10]{};
    /* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
    populate_array(array,10,get_next_value);
    for (auto i = 0; i < 10; ++i) {
        printf("%d ",array[i]);
    }
    printf("\n");
    return 0;
}

请你来说一下fork函数

  • Fork:可以通过fork( )系统调用,创建一个和当前进程映像一样的进程:
  • #include <sys/types.h>
  • #include <unistd.h>
  • pid_t fork(void);
  • 成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。  fork函数会有两个返回值,父进程返回子进程的pid,新创建的子进程返回0,通过返回值确定当前进程是父亲还是孩子。如果出现错误,返回负数。
  • 最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像(起一个进程执行用户指定的程序)。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
  • 在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制
  • 每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得

参考链接

 请你来说一下C++中析构函数的作用

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
  • 析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  • 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
  • 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏
  • 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数
  • 将基类的析构函数弄成虚函数的形式,派生类继承基类的析构函数,并对其进行函数的重载。适用于使用数组存储多个对象的情形,只能使用派生类自定义的析构函数,释放其申请的内存空间。
  • 什么时候需要自定义析构函数?比如在堆上申请了一大段内存空间,使用delete []p,进行资源的释放
  • delete 对象,先调用用户自定义的析构函数,再调用编译系统自动生成的缺省的析构函数
#include <iostream>
#include <fstream>
#include <sstream>
 
class Car{
public:
    Car(){
        m_pName = new char [20];
        std::cout << " 对象创建" << std::endl;
    };
 
    ~Car(){
        delete[] m_pName;
        std::cout << " 对象销毁 " << std::endl;
    }
private:
    char *m_pName;
};
int main(void)
{
    Car *car = new Car();
    delete car;
    car = nullptr;
    return 0;
}

请你来说一下静态函数和虚函数的区别

  • 静态函数在编译的时候就已经确定运行时机虚函数在运行的时候动态绑定虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
  • 类的静态函数是没有this指针的,调用它时不需要创建对象,通过:类名 ::函数名(参数)的形式直接调用。静态函数只有唯一的一份,因此它的地址是固定不变的, 所以编译的时候但凡遇到调用该静态函数的时候就知道调用的是哪一个函数,因此说静态函数在编译的时候就已经确定运行时机
  • 类A与类B构成多态,创建了 A类指针pb指向 B类对象,当程序编译的时候只对语法等进行检测,该语句没有什么问题,但是编译器此时无法确定调用的是哪一个 fun() 函数,因为类A类B中都含有fun函数,因此只能是在程序运行的时候通过 pb指针 查看对象的虚函数表(访问虚函数表就是所谓的访问内存 内存)才能确定该函数的地址,即确定调用的是哪一个函数。这就解释了所说的“虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
#include <iostream>

class A{
public:
    virtual void fun(){
        std::cout << " A " << std::endl;
    }
};

class B : public A{
public:
    virtual void fun(){
        std::cout << " B " << std::endl;
    }
};

int main(void)
{
    A a{};
    B b{};
    A* pb = &b;
    pb->fun();
    a.fun();
    b.fun();
    return 0;
}

参考链接

面向对象的三个基本特征

  • 封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用

封装

  • 封装:就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

继承

  • 继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力

多态

  • 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

参考链接

请你来说一说重载和覆盖

  • 重载:一个类中,两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
  • 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

请你来说一说static关键字

  • 1.加了static关键字的全局变量只能在本文件中使用。例如在a.c中定义了static int a=10;那么在b.c中用extern int a是拿不到a的值,a的作用域只在a.c中。
  • 2.static定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。
  • 3. 对一个类中成员变量和成员函数来说,加了static关键字,则此变量/函数就没有了this指针了,必须通过类名才能访问

 请你说一说strcpy和strlen

  • strcpy是字符串拷贝函数,原型:  char *strcpy(char* dest, const char *src);
  • 从src逐字节拷贝到dest,直到遇到'\0'结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数
  • strlen函数是计算字符串长度的函数,返回从开始到'\0'之间的字符个数

请你说一说你理解的虚函数和多态

  • 多态的实现主要分为静态多态和动态多态静态多态主要是重载,在编译的时候就已经确定动态多态是用虚函数机制实现的,在运行期间动态绑定
  • 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

请你来回答一下++i和i++的区别

  • ++i先自增1,再返回,i++先返回i,再自增1
  • ++i 实现
int&  int::operator++()
{
*this +=1;
return *this;
}
  • i++
const int  int::operator(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}

请你来写个函数在main函数执行前先运行

#include<iostream>

__attribute((constructor))void before()
{
    printf("before main\n");
}

int main(){
    std::cout << "main 函数!" << std::endl;
}

有段代码写成了下边这样,如果在只修改一个字符的前提下,使代码输出20个hello?

  • for(int i = 0; i < 20; i--) cout << "hello" << endl;
#include<iostream>

int main(){
    for(int i = 0; i + 20; i--)
        std::cout << i << " hello" << std::endl;
}

请你来说一下智能指针shared_ptr的实现

参考链接

以下四行代码的区别是什么?

  • const char * arr = "123"; char * brr = "123"; const char crr[] = "123"; char drr[] = "123";

  • const char * arr = "123";  //字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
  • char * brr = "123";  //字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
  • const char crr[] = "123";  //这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
  • char drr[] = "123";   //字符串123保存在栈区,可以通过drr去修改

 请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?

  • 常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化
  • 对于局部对象,常量存放在栈区,
  • 对于全局对象,常量存放在全局/静态存储区。
  • 对于字面值常量,常量存放在常量存储区。
  • 会被人打的方式:#define CONSTANT value
  • 高级一点的方式:const auto constant = value;
  • 最新潮的方式:constexpr auto constant = value;

补充知识

  • constexpr是c++11新添加的特征,目的是将运算尽量放在编译阶段,而不是运行阶段。这个从字面上也好理解,const是常量的意思,也就是后面不会发生改变,因此当然可以将计算的过程放在编译过程。constexpr可以修饰函数、结构体。
  • constexpr用法
  • 才搞清楚常量的存储位置

请你来回答一下const修饰成员函数的目的是什么?

  • const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值