梁越

面试问题-c++基础

0 人看过

一些c++基础问题

关键字

static关键字的作用

如果static修饰的是一个全局变量,那么该变量转化为全局静态变量,只有包含它的文件可见,可以实现隐藏

如果static修饰的是一个成员变量,那么该变量堆此类的所有对象共有,而且只分配了一次内存,每个对象初始化时不会再次对该静态变量重新初始化

如果static修饰的时一个成员函数,和静态成员变量一样,所有类对象共享,但是静态成员函数不依赖类对象,不需要实际初始化一个对象来调用静态成员函数,而且静态成员函数只能访问静态成员变量或者函数,如下面是一个例子:

#include <iostream>
using namespace std;

class Student{
private:
   char *name;
   int age;
   float score;
   static int num;      //学生人数
   static float total;  //总分
public:
   Student(char *, int, float);
   void say();
   static float getAverage();  //静态成员函数,用来获得平均成绩
};

int Student::num = 0;
float Student::total = 0;

Student::Student(char *name, int age, float score)
{
   this->name = name;
   this->age = age;
   this->score = score;
   num++;
   total += score;
}

void Student::say()
{
   cout<<name<<"的年龄是 "<<age<<",成绩是 "<<score<<"(当前共"<<num<<"名学生)"<<endl;
}

float Student::getAverage()
{
   return total / num;
}

int main()
{
   (new Student("小明", 15, 90))->say();
   (new Student("李磊", 16, 80))->say();
   (new Student("张华", 16, 99))->say();
   (new Student("王康", 14, 60))->say();
   cout<<"平均成绩为 "<<Student::getAverage()<<endl;
   return 0;
}

运行结果:
小明的年龄是 15,成绩是 90(当前共1名学生)
李磊的年龄是 16,成绩是 80(当前共2名学生)
张华的年龄是 16,成绩是 99(当前共3名学生)
王康的年龄是 14,成绩是 60(当前共4名学生)
平均成绩为 82.25

virtual关键字的作用

多态的实现,加了virtual的函数为虚拟函数,会根据多态性决定调用基类还是派生类函数,没有virtual会调用基类的方法


虚继承可以可以防止重复引用

const关键字

限定一个变量为只读

当修饰常量,常量值不可修改

const int i=4;
i=5; //无效

当修饰指针

int a = 9;
int b = 10;
const int* p = &a;//p是一个指向int类型的const值,与int const *p等价
*p = 11;    //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b;     //合法,改变了p的指向

当修饰引用

int a = 9;
int b = 10;
int* const p = &a;//p既是一个const指针,同时也指向了int类型的const值
*p = 11;    //编译正确,p不可以修改,但是可以通过*p修改
p = &b;     //编译错误,p是一个const指针,只读,不可变

其实主要是看const后面的变量是什么,只有const后面的变量无法修改


extern关键字

在本文件使用extern声明,可以使用在其他文件定义的变量或者函数


explicit关键字

修饰类的构造函数
防止隐形转化的发生,如下面三种类的声明和初始化发生隐式转换

class Circle
{
public:
    Circle(double r) : R(r) {}
    Circle(int x, int y = 0) : X(x), Y(y) {}
    Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}
private:
    double R;
    int    X;
    int    Y;
};

int _tmain(int argc, _TCHAR* argv[])
{
    //发生隐式类型转换
    //编译器会将它变成如下代码
    //tmp = Circle(1.23)
    //Circle A(tmp);
    //tmp.~Circle();
    Circle A = 1.23;

    //注意是int型的,调用的是Circle(int x, int y = 0)
    //它虽然有2个参数,但后一个有默认值,任然能发生隐式转换
    Circle B = 123;

    //这个算隐式调用了拷贝构造函数
    Circle C = A;

    return 0;
}

//添加explicit
class Circle
{
public:
    explicit Circle(double r) : R(r) {}
    explicit Circle(int x, int y = 0) : X(x), Y(y) {}
    explicit Circle(const Circle& c) : R(c.R), X(c.X), Y(c.Y) {}
private:
    double R;
    int    X;
    int    Y;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Circle A = 1.23; //编译错误

    Circle B = 123;//编译错误

    Circle C = A;//编译错误

    return 0;
}

override关键字

override作为成员函数的修饰关键字,主要作用是避免一些继承的错误,比如,我们看下面这个例子

class A
{
public:
    virtual void func1() const {};
    virtual void func2(int = 0) {};
    virtual    void func3() {};
};

class B :public A
{
public:
    virtual void func1() {}; //编译不会出错,会把当前函数作为一个新的成员函数,而不是继承,继承失败
    virtual void func2(double = 0.0) {}; //编译不会出错,但是参数不同,会作为新的成员函数
    virtual void func4() {}; //编译不会出错,根本就是个新的成员函数,可能是你手抖写错函数名
};

//但是如果你把class B写成下面的形式,就不能通过编译,可以及时发现问题
class B :public A
{
public:
    virtual void func1()  override { cout << "B::func1" <<endl; }; //编译不会出错,会把当前函数作为一个新的成员函数,而不是重写
    virtual void func2(double = 0.0) override { cout << "B::func2" <<endl; }; //编译不会出错,但是参数不同,会作为新的成员函数
    virtual void func4() overide { cout << "B::func3" <<endl; }; //编译不会出错,根本就是个新的成员函数,可能是你手抖写错函数名
};

智能指针

在堆上分配的内存

STL中一般有四种智能指针:

  1. unique_ptr(独享指针),一个独享指针只能占领并管理一个对象;
  2. shared_ptr(共享指针),多个共享指针可以指向一个对象;
  3. weak_ptr(弱指针),这是用来辅助shared_ptr的指针,当shared_ptr管理的对象形成环时,这时候其中一个对象使用weak_ptr可以打破环;
  4. auto_ptr被c++11弃用;

指针和引用的区别

指针有着自己分配的空间,引用只是对象的别名,大小为被引用对象的大小


指针和引用做参数的区别

  1. 两者虽然都可在函数内改变实参值,但是引用毕竟是实参的别名,不占用内存,指针传入的是实参的地址
  2. 并且改变指针不能改变实参地址,改变引用可以改变实参的地址,因为指针做形参传递的是一个拷贝,而形参传递的是一个实参的地址

虚函数表

C++实现虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表(virtual function table, vtbl)

虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址(可以理解为数组里存放着指向每个虚函数的指针)

子类重写父类函数时,实际改变了虚函数表里对应地址


map和set的区别

map是key-value的形式,set只有value,key就是value,所有set不允许有重复值,而map允许有重复值


map与unordered_map和hash_map和multimap的区别

map和multimap底层是红黑数,而unordered_map和hash_map底层是哈希表,红黑书是有序的,哈希表是无序的,而multimap可以具有相同的键值,map键值唯一,一般推荐unordered代替hash_map


strucu和class的区别

在C++中,可以用struct和class定义类,都可以继承。区别在于:struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。


const和#define有什么区别

const有类型,define没有类型


数组与指针的区别

  1. 同类型指针可以相互赋值,数组不行,需要使用下标
  2. 数组开辟连续的内存,大小由元素个数决定,指针的大小64位为8个字节,32位为4个字节
  3. 指针可以寻址,++,–,数组名不行

空类默认有哪些成员函数,类的大小

构造函数、析构函数、拷贝构造、赋值运算符、取址运算符和一个this指针

空类只占一个字节,虚指针4个字节,函数不占字节,继承会继承基类的大小,int占4个字节

class A {}; //一个字节
class B 
{  //一个字节
    void fun(){};
}
class C
{ //4个字节
    virtual void fun(){}; 
}
class D
{ //8个字节
    virtual void fun(){}; 
    int i=0;
}
class E:public D //8个字节
{
}
class E:public D //12个字节
{
    virtual void fun(){};
}

子类继承父类,子类的构造和析构顺序,为什么父类的析构函数需要virtual关键字

子类实例化时先调用父类的构造函数,接下来才是子类的构造函数,析构相反,先是子类,再是父类;如果父类析构不是虚函数,当我们删除父类的指针时,只会调用父类的析构函数,不会调用子类的析构函数,导致父类被删除,子类还存在的现象。


说说公有继承、受保护继承、私有继承和共有成员、受保护成员、私有成员

  1. 公有继承时,派生类对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有和受保护成员,基类的公用成员和保护成员在派生类中保持原有的访问属性;

  2. 私有继承时,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问员;

  3. 受保护继承时,基类的公有成员和保护成员都作为派生类的受保护成员,并且不能被这个派生类的子类所访问,所有基类成员均变成派生类的私有成员。

  4. 一个类的公有成员在任何地方都可以被访问

  5. 一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问

  6. 在没有继承的情况下,protected跟private相同;基类对象不能在外部访问基类的protected成员,派生类中可以访问基类的protected成员;派生类对象如果要访问基类protected成员只有通过派生类对象,派生类不能再外部访问基类对象的protected成员


C++如何阻止一个类被实例化

将构造函数设为private


c++ main函数执行前后

main函数执行前,全局变量和对象的构建,初始化,不接受赋值操作;main函数执行;main函数执行完毕后,回收全局变量和对象,调用析构函数


请描述进程和线程的区别

  1. 进程是系统资源调度分配的一个独立单位;线程是CPU调度和分配的基本单位
  2. 线程运行在线程内,进程是线程的容器,一个进程可以有很多的线程
  3. 进程有独立的内存,同一进程的线程间共享内存,所以进程的切换开销大,线程切换的开销小

new/delete与malloc/free的区别是什么

1、属性:new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持

2、返回:new返回的是指定对象的指针,而malloc返回的是void*

3、参数/内存:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸

4、分配失败:new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL

5、内存区域:new分配内存不一定在堆区;malloc分配内存在堆区

6、new会调用构造函数


内存

请说说c++的内存分配

  1. 栈区(局部变量区):编译器自动编译释放,存放函数参数,局部变量等
  2. 堆区(动态存储区):一般由程序员分配释放,存放的是new/malloc分配的内存块,delete/free释放不过new操作索然一般在堆上分配,但也有在栈上分配的,new的内存分配区抽象来说应该是自由存储区
  3. 全局/静态区:存放全局和静态变量
  4. 文字常量区:存放字符常量等只读数据
  5. 程序代码区:存放函数体的二进制代码

静态内存分配和动态内存分配

静态内存分配发生在栈区,由系统实现,自动释放,比如一个数组

int p[100];

动态内存分配则是在堆上实现,编译器会自动计算,需要手动释放,一般是new关键字和malloc库函数,一样是定义一个一维数组,分别使用malloc和free

#include <iostream>
#include<stdlib.h> //该头文件为malloc必须
using namespace std;

//使用malloc/free动态分配一维数组
int main()
{
    int len;
    int *p;
    cout<<"请输入开辟动态数组的长度:"<<endl;
    cin>>len;
    //长度乘以int的正常大小,才是动态开辟的大小
    p = (int*)malloc(len*sizeof(int));
    cout<<"请逐个输入动态数组成员:"<<endl;
    for(int i=0; i<len; ++i)
    {
        //此处不可以写成:cin>>*p[i]
        cin>>p[i];
    }
    cout<<"您输入的动态数组为:"<<endl;
        for(int i=0; i<len; ++i)
    {
        cout<<p[i]<<" ";
    }
    //时刻记住:有malloc就要有free
    free(p);
 }

//使用new/delete
int main()
{
    int len;
    cout<<"请输入开辟动态数组的长度:"<<endl;
    cin>>len;
    //长度乘以int的正常大小,才是动态开辟的大小
    int* p=new int[len];

    //数据输入
    cout<<"请逐个输入数据:"<<endl;
    for(int i=0; i<len; ++i)
    {
        cin>>p[i];
     }

    //数据反馈
    cout<<"您分配的动态数组为:"<<endl;
    for(int i=0; i<len; ++i)
    {
        cout<<p[i]<<" ";
     }

    //释放内存:
    delete []p;
}

堆和栈的区别

  1. 堆存放的是动态分配的内存,栈一般存放一些静态分配的内存,如局部变量和参数等
  2. 堆的地址是从低到高,栈的地址从高到低,所以栈相对有限制,堆更灵活
  3. 栈由系统自动分配和释放,所以速度快;堆需要手动回收,速度较慢,且容易产生碎片

内存泄露

内存泄漏是指分配了内存却没有回收导致的错误,常见的原因有以下几点:

  1. new或者malloc没有及时delete或free
  2. new []和delete []没有匹配
  3. delete void*指针
  4. 基类的析构函数没有vurtual,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数

内存溢出

程序在申请内存时,没有足够的内存空间供其使用,内存泄漏最终会导致内存溢出


段错误

访问了受保护或者不存在的内存,具体原因有如下:

  1. 数组越界,数据类型不一致
  2. 有些内存是内核占用的或者是其他程序正在使用,比如操作一个文件时,另外一个程序也想要操作这个文件
  3. 野指针或者是指针没有初始化,默认为NULL

野指针

  1. 没有初始化的指针,默认为NULL
  2. 指针被free或者delete之后,没有置为NULL

浅copy和深copy和赋值(深拷贝和浅拷贝)

浅copy只是复制了对象的地址,即指针,和原对象共享同一地址,改变其中一个的值,另一个也会跟着改变;
深copy是将整个对象复制,有独立的地址,两个对象相互独立

赋值和浅copy的区别:赋值是复制了栈中的地址,不论赋值的是基本数据类型还是地址,新旧对象具有关联;浅copy如果复制的是基本的数据类型,那么改变其中一个,另一个不会改变,如果复制的是地址,如数组等,改变其中一个,另一个会发生变化


显式类型转换

  1. const_cast

一般用来区常量化,去const

const int* p;
int *q=const_cast<int*> (p);
  1. static_cast

一个是基本的数据类型之间的转换如int和char
一个是非多态下,子类转基类,基类转子类,多态下的基类转自类不安全,会退出

class A
{
public:
    void fun() {};
};
class B : public A
{

};
class C : public A
{
public:
    virtual void fun() {};
    void fun2() {};
};


int main()
{
    int n = 9;
    char s = static_cast<char>(n); //基本的数据类型转换

    A *a = new A;
    B *b = new B;
    //C *c = new C;
    //D *d = new D;
    a = static_cast<A*>(b); //上行,子类转基类安全
    b = static_cast<B*>(a); //下行,非多态下基类转子类安全
    C *c = static_cast<C*>(a); //下行,多态下,不安全,但是没出错
}
  1. dynamic_cast

和static_cast类似,不过上行下行都是安全的,下行失败返回null,而不是退出

  1. reinterpret_cast

强制类型转换符用来处理无关类型转换的

 int *ptr = new int(233);
 uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);

一个C++源文件从文本到可执行文件经历的过程

1)预编译,预编译的时候做一些简单的文本替换,比如宏替换,而不进行语法的检查;

2)编译,在编译阶段,编译器将检查一些语法错误,但是,如果使用的函数事先没有定义这种情况,不再这一阶段检查,编译后C/C++代码变为汇编代码,得到.s文件

3)汇编,汇编代码变为机器码,得到.o或者.obj文件

4)链接,将所用到的外部文件链接在一起,在这一阶段,就会检查使用的函数有没有定义,链接过后,形成可执行文件.exe


移动拷贝函数、移动赋值函数和拷贝构造函数,拷贝赋值函数

#include <iostream>

using namespace std;

class A
{
public:
    A() { cout << "A构造" << endl; };
    A(char* p) :name(p){ cout << "A带参构造" << endl; };
    ~A() { cout << "A析构" << endl; };

    A(A& t)//拷贝构造函数
    {
        cout << "拷贝构造函数" << endl;
        if (t.name) this->name = t.name;
        //或者深拷贝
        if (t.name)
        {
            int lenth = strlen(t.name);
            this->name = new char[lenth + 1];
            strcpy(this->name, t.name);
        }
    }
    A& operator=(A& t) //拷贝赋值函数
    {
        cout << "拷贝赋值函数" << endl;
        if (this != &t)
        {
            delete name;//删除原有内存
            if (t.name)
            {
                int lenth = strlen(t.name);
                this->name = new char[lenth + 1];
                strcpy(this->name, t.name);
            }
        }
        return *this;
    }

    A(A &&t) //移动构造函数
    {
        cout << "移动构造函数" << endl;
        if (t.name)
        {
            this->name = t.name;
            t.name = nullptr; //t析构更安全
        }
    }
    A* operator=(A &&t) //移动赋值函数
    {

        cout << "移动赋值函数" << endl;
        if (t.name)
        {
            delete name;
            this->name = t.name;
            t.name = nullptr; //t析构更安全
        }
        return this;
    }
private:
    char *name;
};

int main() {
    char *p="ok";
    A a=A(p); //1
    A b; //2
    b = a; //3
    A c = A(a); //4
    A d = move(c); //5
    d = move(b); /6
}

1.带参构造
2.A构造
3.拷贝赋值函数
4.拷贝构造函数
5.移动构造函数
6.移动赋值函数
A析构
A析构
A析构
A析构

c++成员初始化列表为什么会性能更好

对于一般的数据类型,例如int, float, char等在初始化列表和在构造函数内两者的区别并不是很大,但是如果是自定义的类成员,区别就大了,使用初始化列表调用的是拷贝构造函数,而在构造函数中调用的类成员的构造函数和赋值函数,所以在构造函数中需要进行两步,性能也更慢一点

不同情况下类的大小

主要遵循一下几个原则:

  1. 类的静态成员变量不占类的空间
  2. 空类的大小是1个字节
  3. 类不为空的情况下,不计这1个字节大小
  4. 类的虚函数占4个字节
  5. 类的普通成员函数不占空间
  6. 类继承需要把基类大小加上
  7. string占用28个字节
class A1
{
    virtual void fun() {};
};

class A2
{
    virtual void fun() {};
    virtual void fun1() {};
};

class B1 {
    int b;
};

class B2: private A1
{
    int b;
};

class C1{
    void fun() {};
};

class C2
{
    int a;
    void func() { cout << "ok"; };
};

class D1
{
    static int a;
};

class D2
{
    static int a;
    int b;
};

class E
{
    string a="ab";
};


int main()
{
    cout << sizeof(A1)<<endl; //4
    cout << sizeof(A2) << endl; //4
    cout << sizeof(B1) << endl; //4
    cout << sizeof(B2) << endl; //8
    cout << sizeof(C1) << endl; //1
    cout << sizeof(C2) << endl; //4
    cout << sizeof(D1) << endl; //1
    cout << sizeof(D2) << endl; //4
    cout << sizeof(E) << endl;  //28
}

成员列表和构造函数的执行顺序

  1. 成员的初始化顺序与成员的声明顺序相同

  2. 成员的初始化顺序与初始化列表中的位置无关

  3. 初始化列表先于构造函数的函数体执行

什么情况必须使用成员列表初始化

  1. 初始化cost成员变量时
  2. 初始化引用成员变量时
  3. 初始化的自定义的类数据,并且类数据不包含构造函数

内存对齐

按多少字节对齐,首先取alig=Min(所有成员中最大的, 默认对齐方式),默认一般是4个字节

然后每个成员大小是alig的整数倍,并且最后计算的总和也是alig的整数倍

内联函数和宏定义区别

内联函数和宏定义都是在调用的时候进行展开,但是内联函数相对于宏定义有一些优点:

  1. 宏定义相对来说不那么安全,例如在某些场景替换会导致符号优先级错误,得不到意想的结果,这叫做宏的边界效应

  2. 内联函数支持调试,宏不支持

  3. 宏无法访问类的私有成员函数,而内联函数可以自由访问

另外,只有当函数只有 10 行甚至更少时才将其定义为内联函数