宿舍封楼,爆肝两天, C++初阶面试题(未完待续)

256 阅读19分钟

*在宿舍封了一个星期了,前面几天把Linux系统复习完了。

这两天把C++初级的内容小小的复习了一下,写了一下总结。比较简单的总结,不是很详细,但是对于面试还是很有帮助的。

觉得总是看知识点学的还是不透彻,后面要做一下C++的项目才行。

如有不足各位友友可以评论指出,欢迎点赞。*

C++初阶面试题(未完待续)

为什么引用要有初始值?

因为引用是将一个变量的值绑定到引用上的,一步到位,所以要有初始值,普通的定义先定义,再拷贝上去的不用一步到位。

常量不能引用

因为 const修饰就是不能让让它直接或者间接被改变,所以就不能被引用了

引用和指针的区别

根本原因就是引用实际上和被引用的值就是一个空间,但是指针是定义一个变量来存储被引用的变量的地址,是两个空间,所以引用必须初始化,引用使用起来是更加安全的。

内联函数

内联函数一般用于函数调用开销小,但是频繁调用,编译的时候会把这个函数直接嵌入,不会将该语句变成函数指令,没有函数压栈的开销,函数的代码直接被放入符号表中了。像宏一样被展开,inline是一个建议,如果编译器发现这个内联很不适合,那就直接不用

auto

这是一个类型指示符,必须要有初始化,在编译时进行推导的,实际上auto可以视为一个占位符,编译的时候进行替换,声明引用类型的时候必须要加上auto

面向过程与面向对象

关注过程和关注对象,一个是用函数调用解决问题,一个通过对象的交互解决问题

类的两种定义方式

类内定义和类外定义

类内会被自动定义成内联函数,所以建议类内声明,类外定义

封装

隐藏对象的属性和实现细节,仅仅对外公开接口和对象进行交互

类的实例化

类实际上是一个蓝图,类的实例化才是建造模型。

计算类对象的大小

blog.csdn.net/fenxinzi557…

如果定义了很多对象,那么成员变量多份,成员函数多份,这样也太臃肿了,不如变量是多样的,公用一份成员函数,也就是类的成员变量是和成员函数分开的,只有非静态成员变量才属于类的对象上。

  1. 静态成员函数是公共的不属于特定的一个类
  2. 静态成员变量存在静态区,在运行前就已经开辟了空间,不可能与类的对象放在一起
  3. 静态成员函数存在代码区,和非静态成员变量的位置一样
  4. 非静态成员变量是存在栈上的,是类的对象

CPP中允许的函数成员不过是类给函数提供了一个作用域罢了

综合:要计算一个类对象大小就是计算非静态成员变量

当类中有虚函数表的时候,第一个位置实际上是储存虚函数表的地址,所以要额外加上四个字节。

对于单一继承的类对象,先存放父类的虚函数,再存放子类的虚函数,然后是本类的数据

虚函数表中先存放父类的虚函数在存放子类的虚函数

对于重写了父类的虚函数,那么新的虚函数会将虚函数表的父类函数覆盖

字节对齐是以成员变量最大的那个字节对齐的,空类比较特殊用一个字节表示

this

this指针本质上是成员函数的一个形式参数,类对象调用的时候,传递对象地址给this形参,其实对象不储存this指针,通过寄存器来传递的

六个默认构造函数

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值操作重载
  • 取地址操作重载
  • const修饰的取地址重载

构造函数

构造函数可以重载

构造函数不能是const,因为类成员要被改变

如果没有显式的调用,编译器会自动生成合成的构造函数

无参构造函数和全缺省值的构造函数都是默认构造函数,并且默认构造函数只能有一个::也就是说要么就是无参构造函数,要么就是全缺省的构造函数,只能一个

可以带有初始化列表,也可以不带

构造函数的作用是初始化对象,而不是开辟空间

合成的默认构造函数的作用

难道合成的默认构造函数没什么卵用吗?不是

对于语法已经定义的类型比如int,char会自动称为默认值,而对于内置类型,比如内置类,或者我们已经定义好的类,会调用类的默认构造函数实现初始化

析构函数

析构函数对象生命周期结束时,销毁对象的资源。

同样的,系统默认生成的构造函数会对自定义类型执行他的析构函数,从而实现析构

析构的顺序

对象在定义的时候存在栈上,先构造的话,那么就会后被析构

拷贝构造函数

产生对象的副本的时候不再调用构造函数,而是调用拷贝构造函数

拷贝构造函数实际上就是拷贝一个相同的对象

拷贝构造函数实际上也是一种构造函数,拷贝构造函数的参数只有一个,而且必须是引用传参

如果没有拷贝构造函数的话,调用析构函数的时候有可能产生同一块内存连续释放的问题。

ps::编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝,那为什么还要自己定义拷贝构造函数?

因为在涉及内存申请的时候,如果自己不定义拷贝构造函数的话,默认的拷贝构造函数,两个对象是指向同一块内存的,在析构的时候会产生错误。

为什么引用传参

如果不是引入传参,那就是拷贝传参,拷贝传参实际上就是调用拷贝构造函数,那就无穷无尽的调用拷贝构造函数

运算符重载

函数原型:返回值 operator运算符 参数列表

有五个内定的运算符是不能重载的

ps::前置++,和后置++

区别就是后置++,参数列表上面加一个int占位符来区分,而且后置++,要用一个临时值保存,返回这个临时值,只能传值返回

赋值运算符重载

  • 参数类型
  • 返回值
  • 检测是不是自己给自己赋值
  • 返回*this
  • 一个类如果没有自己定义赋值运算符重载,编译器也会生成一个完成对象按字节序的值拷贝

const成员

const修饰成员函数

实际上修饰this指针,也就是说this指向的对象的内容不可以被改变

一些小问题

  1. const对象可以调用非const成员函数吗?

    要用const_cast来去掉const属性

  2. 非const对象可以调用const成员函数吗?

    可以

  3. const成员函数内可以调用其它的非const成员函数吗?

    要用const_cast来去掉const属性

  4. 非const成员函数内可以调用其他的const成员函数吗?

    可以

再谈构造函数

构造函数题赋值

创建对象的时候,编译器通过调用构造函数,给对象的成员变量再函数体内赋值,但是这个并不是对象的初始化,因为初始化只能一次

初始化列表

  1. 每个成员变量只能出现异常

  2. 比如const,引用,这些用初始化列表来进行初始化

  3. 自定义类型成员而且没有默认构造函数的也要在初始化列表

  4. 必须按照变量声明的顺序进行初始化列表

    因为这种对象声明后马上就要初始化,而构造函数只是对他们的赋值而已

  5. 尽量要使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型变量,一定要使用初始化列表初始化

explicit

修饰单个参数构造函数,禁止隐式类型转换

static

类的静态成员,被static修饰的必须要在类外进行初始化

  1. 静态成员被所有对象共享
  2. 静态成员必须类外定义,定义时不加static
  3. 静态成员函数没有this指针,也就是是静态成员函数无法调用非静态函数,因为传不进this指针,要是想调用,那就要把参数设计成this*
  4. 非静态成员函数是可以调用静态成员函数的

C++11成员初始化的新玩法

可以在声明时进行初始化赋值,但是这里是给声明的成员变量缺省值

友元

友元函数可以访问可以直接访问类内的私有成员,这个函数不属于任何类,是属于类外的普通函数,但是它的确是声明在类里面的。加上friend关键字

友元类,实际上是在一个类里面声明一个友元类,这个类的所有成员函数都可以访问另一个类的非公有成员,但是友元关系是单向的

内存管理

int globalVar=1;
static int staticGlobalVar=1;
int main()
{
    static int staticVar=1;
    int localVar=1;
    int num1[10]={1,2,3,4};
    char char[]="abcd";
    char* pChar3="abcd";
    int* ptr1=(int*)malloc(sizeof(int)*4);
    int *ptr2=(int*)calloc(4,sizeof(int));
    free(ptr1);
    free(ptr2);
}

上面的全局变量和static是存储在数据段,函数里面的局部变量都在栈区,申请的空间在堆区,一些代码就在只读的代码区。

C语言的动态内存管理方式

  1. malloc 申请一块空间

    void *malloc(unsigned int num_bytes);
    
  2. calloc是申请可以计算的一块空间,而且会被初始化

    void *calloc(size_t n, size_t size);
    
  3. realloc实际上是对于动态内存进行扩容

    void realloc(void *ptr, size_t new_Size);
    

new &&delete

在申请自定义的类型空间的时候,new会调用构造函数,delete会调用析构函数,而 malloc 和free不会

operator new &&operator delete

operator new:实际上通过malloc来申请空间,申请空间成功的时候直接返回,失败就执行响应的措施

operator delete:实际上通过free来释放空间

new和delete的原理

new

调用operator new来事情空间,在空间上调用构造函数,完成对象的构造

delete

调用析构函数清理空间,调用operator delete来用free释放空间

new T[N]

调用operator new[],调用operator new,完成N个对象空间的申请,然后再完成N构造函数

delete[]

在释放的空间执行N次析构,完成清理,operator delete[]进行释放空间

面试题malloc/free 和new/delete区别

  1. 函数与操作符的区别
  2. 返回值不一样
  3. malloc申请的空间不会进行初始化,new可以初始化
  4. malloc申请失败的时候,返回NULL,要进行判空处理,new只要捕获异常
  5. 对于自定义类型对象malloc只能分配空间,但是new还可以进行构造

什么是内存泄露

应用程序分配某段内存后,因为设计错误,失去对于内存的控制,造成内存的浪费,危害:会导致响应越来越慢,最终卡死

如何一次在堆上申请4G的内存?

void* p=new char[0xffffffful];

因为32位的虚拟地址空间才4G,内核空间就有1G,所以要把执行环境改成64x才行

判断大小端

我们可以设计一个结构体,里面有两个变量,char和int,对第一个变量进行赋值,然后在打印,如果是小端,那么char被赋值,如果是大端,那么int被赋值

union Test
{
int a;
char b;
};
int main()
{ Test t;
t.a=1;
if(t.b==1)
小端
else
大端
​

字符指针在32位下,int是4个字节,char是一个字节,我们让int 的变量赋值为1,然后如果把这个变量的地址强转成插入,就会发生截断,如果第一个字节是1,那么就是小端了

int  a=1;
if((char)&a==1){
小端
}
else{
大端
}
​

模板

template<typename/class t>

隐式实例化

让编译器根据实参推演模板的实际类型

显式实例化

比如Add(a,b)

当一个非模板函数和一个同名的函数模板同时存在,而且该函数模板还可以实例化这个非模板函数会优先调用非模板的函数

类模板

template<class T1,class T2,...,classs Tn>

String

string的模拟实现,深拷贝

class Mystring{
    private:
    char* str_;
    public:
    Mystring(const char* str="")
    {
        if(str==nullptr)
        assert(false);
        str=new char(strlen(str)+1);
        strcpy(str_,str);
    }
    Mystring(const Mystring& s):str_(new char(strlen(s.str_)+1))
    {
        strcpy(str_,s.str_);
    }
    Mystring& operator=(const Mystring& s)
    {
        if(this==&s)
        return *this;
        else
        {
            char* pstr_=new char(strlen(s.str_)+1);//先开辟一块空间
        strcpy(pstr_,s.str_);//把内容考进去
        delete(s.str_);
        str_=pstr_;//把地址赋值进去
        return *this;
        }
        
    }
    ~Mystring()
    {
        if(str_)
        {
            delete[] str_;
            str_=nullptr;
        }
    }
};

coolshell.cn/articles/10…

写时拷贝

写时拷贝就是在浅拷贝的基础上,为一块内存添加一个引用计数,初始为1,要是增加了一个对象,那就+1,销毁对象那就-1,然后检查要不要释放资源,要是计数是1,那说明这是最后一个使用者,可以释放资源,否则不能。

vector

可以理解为可变空间的数组,而且尾插和尾删的效率比较高,和其他容器而言

vector的空间增长

resize 改变向量的size,而且还会进行初始化

reserve 改变容量

对于 GCC 和G++ 分别是以1.5倍和2倍增长的

增删改查

insert是在pos位置之前插入

operator[] 向数组一样访问

迭代器失效问题

迭代器实际上就是一个指向一段空间的指针,当这块空间消失的时候那么就会引起迭代器失效的问题

  1. 会引起底层空间改变的操作,都可能引起迭代器失效,比如对于空间进行扩容,会引起原来的空间倍销毁,那么原来的迭代器也就失效了。
  2. 对于指定位置的删除,删除指定位置的内存,后来的内存就会被补上来,理论上不会迭代器失效,但是如果补上的位置正好是没有元素的,那么迭代器就失效了
  3. 解决迭代器失效的问题,就提前给迭代器赋值即可

list

实际上是用双链表实现的顺序存储结构,因为是双链表可以实现任意位置的插入和删除,但是因为内存不连续,不可以随机访问,必须迭代访问才行

list的迭代器失效的问题

list中插入是不会导致迭代器失效的,只有删除,而且只有被删除的元素的迭代器才会失效。后面的迭代器不会失效

vector和list的对比

vector:一块连续的空间,支持随机访问,只有尾删和尾插效率比较高,任意地方的更改效率不高,可能会一起增容,内存比较一致,不会引起内存碎片

list:不连续的一段空间,不支持随机访问,只能迭代访问才行,可以任意位置更改,不会产生批量增容的问题,会产生内存碎片的问题。

stack and queue

主要是leetcode上面的算法题

stack的模拟实现

栈实际上是一种特殊的vector,因此vector可以完全模拟栈

queue

[leetcode.cn/problems/im…]:

这个用队列实现栈,要注意实现两个队列,队列什么是头,什么是尾,而且要注意,当一个队列把值给另一个队列的时候,实际上顺序可以说是一样的,这题要控制转移的值是还剩一个,而且还要注意万一就插入一个值,还要注意将另外一个队列给清空,虽然是个简单题,但是要注意的还是很多。

queue的模拟实现

queue的底层可以用deque来实现,也可以用list来实现

优先级队列 priority_queue

优先级队列是一种容器适配器,它的第一个元素总是它包含的元素中最大的。

上下文类似于堆,堆中可以随时插入元素,并且只能检索最大堆的元素

所有需要用到堆的位置,都可以考虑优先级队列

priority_queue.top();----返回最大元素

如果创建小堆,就要使用仿函数#include<functional.h>,比如我现在有一个数组V,我想创建V2的小堆的优先级队列

priority_queue<int,vector<int>,greater<int>> V2(V1.begin(),V1.begin());

topK

[leetcode.cn/problems/kt…]  "leetcode" 

适配器

适配器实际上是一种设计模式,这种模式是将一个类的接口换成客户希望的接口

双端队列

双端队列兼顾了vector和list的优点,不仅头插效率高,而且于list相比空间利用率比较高,deque并不是连续的空间,而是一段段连续的小空间,拼接而成的,类似一个动态的二维数组,deque的缺陷,而且是致命的缺陷,不适合遍历,因为遍历的时候,deque的迭代器要频繁检测是否移动到了某段小空间的边界,所以我们在大部分场景中,用到遍历场景还是比较多的就不喜欢用双端队列,双端队列的迭代器设计的极为复杂。

为什么选用deque作为栈和队列的默认容器

因为栈和队列无需进行遍历,往往都是访问头尾的元素,完美的避开了双端队列的缺陷。

再谈模板

模板可以使用非类型的参数,比如

template<class T,size_t N=10>

模板的特化

template <class T>
int compare(const T &left, const T&right)
{
    std::cout <<"in template<class T>..." <<std::endl;
    return (left - right);
}

这个函数不满足我们的要求,因为它不能支持char*(string)类型

所以我们必须对它进行特化,可以让它支持两个字符串的比较,因此实现如下的特化函数

template < >
int compare<const char*>(const char* left, const char* right)
{
    std::cout <<"in special template< >..." <<std::endl;
​
    return strcmp(left, right);
}

函数模板的特化,当函数发现有特化后的匹配函数的时候,会优先调用特化的函数,而不再通过函数模板来进行实例化

偏特化

也就是对模板参数进一步进行条件限制设计的版本,比如对于以下模板类

template<class T1,class T2>
class Data
{
    public:
    private:
    T1 _d1;
    T2 _d2;
}

我们可以对其中的参数进行限制,比如这样将第二个参数特化成int

template<class T1>
class Data<T1,int>
{
    ......
}

也可以这样,比如都让传入指针的形式

template<typename T1,typenname T2>
class Data<T1*,T2*>
{
    ......
}

模板的分离编译

如果模板的声明和定义分离开,在头文件中进行声明,源文件中完成定义

template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}
// main.cpp
#include"a.h"
int main()
{
 Add(1, 2);
 Add(1.0, 2.0);
 
 return 0;
}

在一般的函数是可以支持分离编译的,CPP文件在编译的时候是先把.h文件编入进来在进行编译,编译器将一个工程中的所有CPP文件分离的形式进行编译后,再有连接器进行连接称为可执行文件

不如我们的main.cc中调用函数f(),它所知道的是并入的.h文件里面有这个函数的声明,也就是认为函数实现代码在另外一个CPP编译完成的.obj文件中,在另一个文件果然找到了这个函数,完成程序的执行。

但是对于模板就不一样了,模板的函数代码不能直接编译成二进制代码,而是先实例化,实例化的时候,只看到了函数的声明,但是没有函数的定义,就无法完成实例化,那就只能通过在其他的obj文件里面找到这个实例,但是其他obj文件根本就没有这个实例,那就更无法编译成.obj文件执行后续的链接工作了。

当模板的声明和定义放在一起的时候,编译主函数的时候给出一个指示,指示带有模板定义的.cpp编译时生成实例化函数,然后在链接的时候就可以找到这个被实例化的函数。

简单的来说,就是在函数模板包含的cpp文件里面如果没有对于这个模板进行实例化,那么编译就根本没有这个函数的实现,主函数在执行函数是,称为obj文件之后,想要找到这个函数的地址,但是根部找不到,链接时找不到函数,所以链接时错误了。

解决方法:将声明和定义放在一起,这样的化,编译主函数的时候,编译器不知道函数的实现,但是直到这个函数在哪,给出一个指示,这样编译test.cpp的时候,实例化f出现了在test.obj中,在链接的时候,主函数在另一个cpp文件中找到了它所调用的函数地址。

模板的缺陷,会导致代码膨胀的问题,也会导致编译时间边长,而且当模板便器错误的时候,错误信息非常凌乱,不易定义错误。

C++的IO流

标准IO流

  • cin:标准输入
  • cout :标准输出
  • cerr:标准错误
  • clog:标准日志

要注意的是内置类型可以随意插入和提取,但是自定义类型我们要重载流插入和流提取

文件IO

  1. 我们可以定义一个文件流对象,比如ifstream ifile 这个流文件可以被写入,比如cin 可以向标准输入里面写入也可以ofstream ofile,比如cout,标准输出可以向外部写入, 这个流文件可以向外部写入,也可以定义读写文件 fstream iofile
  2. 然后我们使用文件流对象的成员函数来打开一个磁盘文件,使文件流对象和磁盘建立联系
  3. 使用流插入或者流提取或者成员函数向文件读写
  4. 最后关闭文件

stringIO

我们同样可以定义一个istringstream 的对象,其他的数据向这个对象里面写入数据就会自动变成string类型的了,也可以定义一个ostringstream的对象,这个对象向string对象里面写入,对于stringstream可以对string对象进行读写

我们可以进行字符串的拼接

int main()
{
 stringstream sstream;
 // 将多个字符串放入 sstream 中
 sstream << "first" << " " << "string,";
 sstream << " second string";
 cout << "strResult is: " << sstream.str() << endl;
 // 清空 sstream
 sstream.str("");
 sstream << "third string";
 cout << "After clear, strResult is: " << sstream.str() << endl;
 return 0;
}

也可以把数值类型格式化为字符串

int a=1234;
string sa;
stringstream s;
s<<a;
s>>sa;

优点是无需考虑空间的问题,因为底层是string而不是字符数组.

         ** 未完待续**