C
1.const的作用有哪些,谈一谈你对const的理解?
const关键字的语义是希望其修饰的变量不会被改变,在变量的作用域中保持固定。希望手动增加一些限制,当对const变量进行修改时,编译器就会报错。一般来说const都是用来修饰函数的参数,为了防止函数在内部修改指针指向的数据。
-
定义变量
-
常量在定义的同时进行赋值(初始化),因为常量一旦被创建就无法修改
-
int main(){ const int a = get_num(); //运行时初始化 const int b = 10l //编译时初始化 }
-
-
定义指针
const int* p1
,离变量名远,指针本身可以修改指向不同的数据,但是被指向的数据无法修改int const * p1
,指针指向和数据都无法修改
-
定义函数参数
-
为了防止函数在内部修改指针指向的数据
-
size_t strlen ( const char * str );
-
-
const与非const转换
- const是只读不能写的,非const是可读可写的(权限高),从非const转成const完全没问题(降低权限),但是const转非const就不行了,因为原本const是不让修改的,转成非const后就达到了可修改的意思。(函数参数是非const类型–可能会修改数据,传递一个const类型的数据进来就不对了)
2.static的作用是什么,什么情况下用到static?
-
隐藏
-
全局变量和函数的作用域是整个程序,也就是所有的源文件可以访问,但是会导致命名冲突。使用static修饰变量或者函数时,能够将作用域限制在本模块中(也就是文件中)。
-
//这样就不会冲突了 //epoll_poller.cc static const int kInitEventListSize = 16; //kqueu.cc static const int kInitEventListSize = 16;
-
-
变量内容的持久化
-
static修饰局部变量,存储在全局数据区,全局数据区的数据在程序启动的时候就被初始化(单例类实现-构造),直到程序结束才会被释放。
-
int func(){ static int n = 0; //程序启动时就被初始化了。也可以不赋初值 0,静态数据区的变量默认初始化为 0 n++; //只有在fun函数的作用域内才能访问到这个static变量 printf("Function is called %d times.\n", n); return n; }
-
3.全局变量和局部变量的区别?
-
在函数内部定义的变量称为局部变量,作用域仅限于函数内部,离开函数作用域后再使用就会报错。
- main函数中定义的变量也是局部变量,只能在main函数中使用。main函数也是一个函数,和其他函数平级而已。
- 形参、函数内部定义变量都是局部变量,实参给形参传值的过程就是给局部变量初始化。
-
函数体外部定义的变量叫做全局变量,作用域默认是整个程序。
-
全局变量有先后位置之分。b在fun1之后定义的,所以在fun1中无效。
-
int a; void fun1(){//todo } int b; void fun2(){//todo }
-
4.宏定义的作用?优缺点?
宏定义的作用:
- 条件编译
- 宏函数
- 记得括号包裹,不然容易出问题
- 一些代码逻辑片段可以用do{}while(0)来包裹住,因为又不是真的函数。
- 字符串替换,在预处理阶段做替换,不涉及类型检测等手段。
- 节省一些函数调用的开销,同时避免了函数调用时参数传递造成的内存开销。
5.内存对齐的概念?为什么会有内存对齐?内存对齐的原则?
32位cpu可以一次性读取4个字节,64位cpu则可以读取8个字节,但是有一个前提,就是这4个字节或者8个字节,必须在四字节边界或八字节边界。也就是说地址必须是4N或者8N。不对齐,你的数据跨两块读取区域就要搞两次。有的CPU直接就异常了(不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常)
使用内存对齐的情况:
未使用内存对齐:
原则:
-
原则1:对于struct和union来说,其第一个数据成员要放在offset==0的地方。如果第一个数据成员为某个复合结构的子成员,则要根据子成员的类型存放在对应的整数倍的地址上
-
原则2:结构体成员按自身长度自然对齐(所谓自然对齐,指的是该成员的起始位置的内存地址必须是它自身长度的整数倍)。如果结构体作为成员,则要找到这个结构体中的最大元素,然后从这个最大成员长度的整数倍地址开始存储
-
原则3:结构体的总大小为结构体的有效对齐值的整数倍
-
当未明确指定时,以结构体中最长成员的长度为其有效对齐值
-
当用
#pragma pack(n)
指定时,以n和结构体中最长成员的长度中较小者为其有效对齐值-
#pragma pack(push,1) //1可以改为2,4,8... //这里设置结构体按一字节对齐 struct Test{ char t1; //1 short t2;//2 double t3;//8 }; #pragma pack(pop)
-
-
当用
__attribute__ ((__packed__))
指定长度时,强制按照此值为结构体的有效对齐值
-
6.inline内敛函数的特点有哪些?它有什么优缺点?
- 内敛函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收、结果返回等步骤,提高了运行速度
- 同宏函数相比,在代码展开时,会做安全检查或者自动类型转换,而宏函数不会。
- 在类中进行声明和定义的成员函数,自动转换成内敛函数,因此内敛函数可以直接访问类的成员变量,宏函数不可以。
- inline函数方便调试,宏函数无法调试..
- 如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。
- nline函数只是对编译器的建议,是否对函数内联,决定权在于编译器
- inline会造成代码膨胀,以空间换时间的方式。
7.如何避免野指针?
野指针是指向不可用内存区域的的指针,不是null指针。人们一般不会错用NULL指针,因为if语句能够判断。但是野指针是很危险的,if不能判断一个指针是正常指针还是野指针。(野指针是有地址的!)
出现野指针的原因:
- 指针变量没有初始化。任何指针变量被创建时并不会自动称为NULL指针,它的缺省值时随机的,所以初始化的时候要么置为null,要么指向合法的内存
- 指针p被free/delete后,没有置为NULL。free和deletes只是把指针指向的内存给释放掉,并没有把指针本身干掉。
- 不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。(可以理解成指向了局部变量)
8.如何计算结构体长度?
需要考虑内存对齐的问题。
9.sizeof和strlen有什么区别?
-
sizeof是关键字,而strlen是一个函数,sizeof的参数可以是数组、指针、类型、对象、函数等等
-
数组的话就是数组的大小,但是切记,当把数组传递给函数时,数组就变成了一个指针…,大小就是指针的大小了…
-
指针的话取决与cpu的位数,32位就是4,64位就是8
-
可以用来计算数据大小,但是记得除以类型
-
int arr[] = { 1,2,3,4,5,6,7 }; int _cnt = sizeof(arr) / sizeof(int);
-
-
-
sizeof返回的是size_t,一个无符号数,if(sizeof(x)- 5>=0)这种判断就永远为真了…
-
sizeof的计算发生的编译时,因此 sizeof 不能用来返回动态分配的内存空间的大小
-
strlen遇到
\0
就计算结束了
10.知道条件变量吗?条件变量为什么要和锁配合使用?
什么要引入条件变量?
借用生产者消费者问题模型来说明。因为生产者和消费者并没有沟通渠道,消费者不知道产品什么时候做好,这个时候消费者就需要一直去询问商店老板是不是有货了。这就是最简单的轮询的模式,但是这种模式会导致cpu占有率贼高。为了解决这种问题,为生产者和消费之间提供一个沟通渠道,就提出了条件变量的手段。
消费者来到商店后,发现没有商品,于是选择等待,等生产者通知有货后再来购买。当生产者生产了一批产品后,发送一个我已经生产完毕的信号给消费者,消费者就前来购买。这样就极大的提高了效率。
void *thread_producer(void *arg)
{
inser_msg_to_queue(msg_queue, data);
}
void *thread_consumer(void *arg)
{
while(1){
if(!msg_queue_is_empty(msg_queue)){
data = get_msg_from_queue(msg_queue);
}
}
}
为什么要配合锁来进行使用?
条件变量本身只是描述了一种工作队列的状态,也是一种资源。任然回到生产者消费者模型,条件变量是一种沟通渠道(电话),那么如果没有约束,是不是所有的消费者都可以来抢购?可能会导致本应该购买到商品的消费者没有拿到数据?
- 条件变量用于标识工作队列的状态。仅仅代表工作队列中有没有工作节点、工作队列状态变化,没有其他额外的含义,这本来就是它的设计初衷,只不过类似于linux链表结构 struct slist_s 设计一样,跟业务解耦了,开发者爱怎么用就怎么用,条件变量就是代表开发者自己的工作队列状态。
- 既然条件变量是标识工作队列的状态,那么我们在工作队列中的操作中使用了条件变量,这个条件变量就变成了工作队列的一部分,而工作队列一定是个共享资源,所以对于工作队列的操作,最好是要配合着互斥锁使用的,因为这样安全、有序。
pthread_cond_wait做的事情:(原子的)
- 自动把调用线程放到等待条件的线程队列上
- 对互斥量解锁
当条件变量发生变化时,pthread_cond_wait会返回:
- 条件变量状态变化时,内核收到pthread_cond_signal信号,会从等待条件的线程列表里,按照内部调度算法,唤醒至少1个线程。
- 被唤醒的线程中,pthread_cond_wait函数返回,其余继续阻塞,并且互斥量再次被锁住,确保当前线程的操作是安全的。
11.如何用C实现C++的面向对象特性
参考:
12.memcpy怎么实现让它效率更高?
- 一次拷贝4字节?或者8字节
- 拷贝内存重叠问题(使用尾拷法)
13.typedef和define有什么区别?
- 执行上不同:typedef在编译器,具备类型检测的功能。define是宏定义,发生在预编译,只是字符串替换,不做任何检查
- 功能差异:typedef是定义类型的别名。define不只可以为类型取名字,还可以定义常量、变量、编译开关等
- 作用域不同:typedef有自己的作用域,而define没有
14.extern有什么作用,extern C有什么用?
extern:
- 前置声明,告诉编译这个玩意的定义在其他.c文件中(类似于头文件的作用)
- 导出全局变量,能够在其他.c文件中进行使用
extern “C”:
extern"C"的最根本的作用,就是为了让C的函数按照C的方式来编译链接,不会因为C++的重载机制,导致C的函数编译后函数签名变化,这样在C++代码里面调用C函数时能够正确的链接到。
void func(int a,int b);
c:编译出来就是__func
c++:编译出来是__func_int_int
在调用的时候,请求的是 __func,如果不用extern “C”,那g++就找不到 __func这个函数了…
15.字符串函数?
字符串是\0
结尾。
strlen
:计算字符串的长度,该长度不包含\0
.strcpy(dst,src)
:字符串拷贝- 会拷贝源字符串中的
\0
到目标空间,前提是源字符串有 - 目标空间必须比源字符串空间大,否则会出现访问越界
- 会拷贝源字符串中的
16.memcpy和memmove有什么区别?
void *memcpy(void *dst, const void *src, size_t n );
void *memmove(void* dst, const void* src, size_t n);
二者都是讲字src的n个字符拷贝到dst,memcpy默认不存在内存重叠,而memmove则会考虑这种情况。
src地址小于dst,拷贝前3个字节没问题,但是4、5字节(属于dst,所以一开始就被覆盖)被src自己覆盖了,就会导致拷贝数据有问题。memmove会对拷贝的数据作检查,确保内存没有覆盖,如果发现会覆盖数据,简单的实现是调转开始拷贝的位置,从尾部开始拷贝
#include <stddef.h> /* for size_t */
void *memmove(void *dest, const void *src, size_t n)
{
unsigned char *pd = dest;
const unsigned char *ps = src;
if (__np_anyptrlt(ps, pd)) //if (dst <= src || (char*)dst >= ((char*)src + count)) __np_anyptrlt检测是否有内存重叠
// 重叠,尾拷法
for (pd += n, ps += n; n--;)
*--pd = *--ps;
else
// 不重叠,正向拷贝
while(n--)
*pd++ = *ps++;
return dest;
}
17.线程安全与可重入?
线程安全:
多个线程同时运行一段代码,预期结果都一样,那么就是线程安全的。一般来说线程不安全的函数:
- 不保护共享变量的函数(线程共享地址空间和资源=static变量、全局变量。局部变量无所谓,因为进程有自己的栈和上下文)
- 调用线程不安全的函数
可重入:
以一个单线程应用为例,假如timer注册的callback与主线程逻辑都会调用某一个函数func,那么我们就必须考虑这个func函数是否可重入。因为会有这样的问题:线程执行func函数到一半,timer的callback被触发,而callback也调用了func函数,于是func函数被重复进入,也就是重入了。要保证func的正确性,就是func不能使用一些全局性的东西(被大家所有共有)。
不可重入的特点:
- 调用了了malloc/free函数,因为malloc使用全局链表来管理堆的
- 调用标准I/O库函数
- 使用了static变量
可重入函数是线程安全函数的一种,线程安全不一定可重入,可重入函数一定是线程安全的。
线程安全—–包括—>【可重入】
参考:
可重入(reentrancy) vs. 线程安全(thread-safety) - 知乎 (zhihu.com)
18.访问空指针会发生什么?
访问空指针:Linux 中,每个进程空间的 0x0 虚拟地址开始的线性区(memory region)都会被映射到一个用户态没有访问权限的页上。通过这样的映射,内核可以保证没有别的页会映射到这个区域。
- 编译器把空指针当做 0 对待,开心地让你去访问空指针。
- 缺页异常处理程序被调用,因为在 0x0 的页没有在物理内存里面。
- 缺页异常处理程序发现你没有访问的权限。
- 内核发送 SIGSEGV 信号给进程,该信号默认是让进程自杀。
一般情况下的访问:
进程使用的是虚拟地址,虚拟地址需要与物理地址进行映射才能被使用。如果访问了没有被映射的虚拟地址,cpu会触发内存访问异常,并且调用异常处理程序对没有被映射的虚拟地址进行映射操作。linux的内存访问异常处理函数是do_page_fault()。
当异常发生时,CPU会把触发异常的虚拟内存地址保存到 cr2寄存器
中,do_page_fault()
函数首先通过读取 cr2寄存器
获取到触发异常的虚拟内存地址,然后调用 find_vma()
函数获取虚拟内存地址对应的 vm_area_struct
结构,如果找不到说明这个虚拟内存地址是不合法的(没有进行申请),所以内核会发送 SIGSEGV
信号(传说中的段错误)给进程。如果虚拟地址是合法的,那么就调用 handle_mm_fault()
函数对虚拟地址进行映射。
19.free传入了void*指针,是如何知道需要释放多大的内存?
申请内存的时候,实际会大一些,header存了这块内存有多大的信息
____ The allocated block ____
/ \
+--------+--------------------+
| Header | Your data area ... |
+--------+--------------------+
^
|
+-- The address you are given
20.c语言函数调用过程?
函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。 栈指针和帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器做栈指针。
帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶。
21.判断大小端
大端方便人阅读
bool is_big_endian2(){
int a = 0x12345678;
char *tmp = &a;
return ((tmp[0]==0x12) && (tmp[1]==0x34) && (tmp[2]==0x56) && (tmp[3]==0x78));
}
C++
C++11标准
1.聊一聊c++11?平时都用到哪些特性?
- 右值引用和移动构造
- 初始化列表(构造函数)
- 统一初始化(initializer_list)
- auto类型推导(方便写代码,但是不太好阅读)
- 范围for
- lambda函数
- nullptr:解决NULL的二义性问题
- explict,避免隐式转换,需要手动触发构造函数
- enum class,强类型枚举
- 智能指针
- 线程库支持
2.聊一聊智能指针?
智能指针主要是借助RAII的思想,把资源交给对象来进行管理。
unique_ptr
:拷贝构造函数、赋值运算符都被禁用了。所以只可以使用移动xxx系列了。shared_ptr
:纯正的引用计数器- 具体结构
weak_ptr
:弱引用- 配合shared_ptr解决循环引用的问题,但是weak_ptr也有一个缺陷:虽然通过weak_ptr指针可以有效的解除循环引用, 但这种方式必须在程序员能预见会出现循环引用的情况下才能使用, 也可以是说这个仅仅是一种编译期的解决方案, 如果程序在运行过程中出现了循环引用, 还是会造成内存泄漏。
- A的shared_ptr指向B,B的shared_ptr指向A。A、B不需要存在继承关系
语法基础
0.引用和指针有什么区别?
什么是引用?
引用可以看成是数据的一个别名,并不需要分配内存。type &a == data
引用有什么优势?
引用主要目的在函数参数中,当函数参数是类类型时,普通值传递会导致调用拷贝构造函数,影响效率。
int func(const T& a){
//只是希望使用a里面的数据来做一些运算,我并不需要a这个玩意本身.
//普通方式传递会导致发生一个拷贝,这个拷贝完全只是拷贝了一些数据而已,很显然这个拷贝是多余的,所以使用引用。
//
//引用直接使用最初的对象,不会发生拷贝.
//加const表示我只是使用你的数据,不会修改.
}
引用的运用场景?
1>作为函数入参,效果和指针一样,对参数的修改会直接作用到传入的实参上
2>作为函数返回值,type& func(){},最大的好处就是在内存中不会产生返回值的副本
同指针的区别?
a,返回动态分配的内存或对象,必须使用指针接收
b,指针有自己的存储空间,而引用只是一个别名,并无具体内存空间
c,指针的大小取决于cpu位数,引用取决于被引用对象的大小【使用sizeof()】
d,指针可以改变指向,但是引用不行,引用在初始化时就指定了对象,中途无法修改。
1.const关键字有什么用?
基础的
修饰引用:
常作为函数的形参,防止函数内对象被意外修改。这就是为什么拷贝构造函数的参数定义为常引用,为了保护被拷贝的对象不会被修改。
class Test{
public:
Test(cosnt Test& a);
private:
int a_;
};
修饰函数返回值:
其实和const修饰普通变量和指针一样,防止外部对object进行修改。
const int& func() // 返回的指针所指向的内容不能修改
{
// return p;
}
和oop相关的
修饰类的成员函数:
- 修饰成员函数时,表示这个成员函数不能修改任何成员变量!!!
class Entity{
private:
int x;
int y;
public:
void getX() const{
//x = 2; 这样是不行的
return x;
}
void getX(){
return x;
}
};
修饰成员变量:
- 修饰成员函数的参数时,表示这个参数不能改动
void func(const Entity& e){
e.x = 1; //这是不允许的!
e.getX(); //此时调用的是带const修饰的,因为参数e约定好了是不能被修改. 没有const修饰就不行,func会认为e可能被修改.
}
修饰类对象:
- 修饰类对象时,该对象内的任何成员变量都不能被修改。
- 不能调用任何非const的成员函数,因为非const成员函数的调用有机会修改成员的机会
2.static关键字有什么用?
基础用法同c一样,对于oop来说有一些不一样:
修饰成员变量:
这个static成员变量需要类外赋初值,因为它在程序启动的时候就初始化好,存储在全局数据区,存在整个程序的生命周期中。可以通过对象来访问,也可以使用类来进行访问。一般都会定义成public。
class test {
public:
void display() {
printf("%d", val_);
}
public:
static int val_;
};
int test::val_ = 1; //必须得初始化,不然报链接错误。
修饰成员函数:
类似的,这个函数属于类而不是某一个对象。static函数只能访问static变量(普通成员函数能访问所有),不能访问成员变量,因为它不属于某一个对象。
3.malloc和new有什么区别?
-
申请内存的位置:malloc是在堆上,new是在C++的抽象概念上的自由存储区,具体在什么上面看operator new的实现。
-
返回类型的安全性:malloc是直接返回void*指针,需要我们强制转换。new返回对象类型的指针。
-
内存失败时候的返回值:malloc是返回NULL,new会抛出bad_alloc异常
-
指定申请内存的大小:malloc需要指定申请内存的大小,而new不用,编译器会根据类型信息自动计算
-
调用构造/析构函数:malloc不会调用,new则是调用operator new分配内存,然后调用构造函数构造对象,并为其传入处置,构造完成以后返回一个指向该对象的指针。释放的时候调用析构,然后operator delete来释放内存空间。
class A { public: A() :a(1), b(1.11){} private: int a; double b; }; int main() { A * ptr = (A*)malloc(sizeof(A)); return 0; }
-
对数组的处理:new[]/delete[],int *ptr = (int*)malloc( 10* sizeof(int)); 定义一个大小为10的数组
-
new和delete这两个关键字是可以被重载的!!!
-
malloc分配内存后如果发现内存不够用,可以调用realloc重写分配内存实现内存扩充。realloc首先判断当前指针所指内存是否还有足够的连续空间,如果有则直接扩大并返回原来的指针地址。如果没有则重写申请内存,并把原来的内容拷贝一下。
malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种。而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施给你使用。
4.四种类型转换?
- static_cast:
- 用于非多态类型的转换
- 常用于数值数据转换
- 子类指针转为父类安全,父类转子类不安全
- dynamic_cast:
- 用于多态类型转换
- 只适用于指针或引用
- 父类-子类指针/引用可以互转
- const_cast:
- 用于删除const、volatile和__unaligned特性
5.new、operator new和placement new?
new和operator new:
- new是一个关键字
- operator new是一个操作符,可以重载的。
当我们执行A* tmp = new A()时候,我们知道这里分为三步:1.分配内存,2.调用A()构造对象,3. 返回分配指针。事实上,分配内存这一操作就是由operator new(sizeof(A))来完成的,如果类A重载了operator new,那么将调用A::operator new(sizeof(A) ),否则调用全局::operator new(sizeof(A) ),后者由C++默认提供。总结步骤:
- 调用operator new(sizeof(A))申请内存
- 调用A的构造函数创建对象
- 返回指针
operator new有三种形式:
//throwing (1)
void* operator new (std::size_t size) throw (std::bad_alloc);
//nothrow (2)
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
//placement (3)
void* operator new (std::size_t size, void* ptr) throw();
第三种,placement new是对operator new的一个重载,定义在#include<new>
中。
new的第三种形态——placement new是用来实现定位构造的,因此可以实现new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象,这有点类似于前面代码中的“p->A::A(3);”这句话,但这并不是一个标准的写法,正确的写法是使用placement new:
#include <new.h>
void main()
{
char s[sizeof(A)];
A* p = (A*)s;
new(p) A(3); //p->A::A(3);
p->Say();
}
这里“new(p) A(3)”这种奇怪的写法便是placement new了,它实现了在指定内存地址上用指定类型的构造函数来构造一个对象的功能,后面A(3)就是对构造函数的显式调用。这里不难发现,这块指定的地址既可以是栈,又可以是堆,placement对此不加区分。但是,除非特别必要,不要直接使用placement new ,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步骤而已。使用new operator地编译器会自动生成对placement new的调用的代码,因此也会相应的生成使用delete时调用析构函数的代码。如果是像上面那样在栈上使用了placement new,则必须手工调用析构函数,这也是显式调用析构函数的唯一情况: p->~A(); 当我们觉得默认的new operator对内存的管理不能满足我们的需要,而希望自己手工的管理内存时,placement new就有用了。STL中的allocator就使用了这种方式,借助placement new来实现更灵活有效的内存管理。
(11条消息) C++ 内存分配(new,operator new)详解_wudaijun的博客-CSDN博客_hsv色彩空间
6.值传递、指针传递、引用传递?
- void func(int a,int b);
值传递。形参在函数体内部的任何操作都不会影响到实参。
func(1, 2);
-
void func(int *c,int *d);
指针传递。本质上还是值传递,只不过在函数内部栈中开辟的空间是存放的实参的地址。
#include<iostream> void func(int* a, int* b){ std::cout<<&a<<" "<<a<<" "<<*a<<std::endl; //此时a是实参的地址了嘛,所以*a就可以表示操作实参了! std::cout<<&b<<" "<<b<<" "<<*b<<std::endl; } int main(){ int a = 1; int b = 2; std::cout<<&a<<" "<<a<<std::endl; std::cout<<&b<<" "<<b<<std::endl; func(&a, &b); return 0; } // 0x7fff4d64d1d0 1 0x7fff4d64d1d4 2 0x7fff4d64d1b8 0x7fff4d64d1d0 1 0x7fff4d64d1b0 0x7fff4d64d1d4 2
-
void switchInt(int &c,int &d);
传引用。在函数内部栈中并不会开辟内存空间!
7.c++函数返回局部变量的std::move问题?
链接:https://www.zhihu.com/question/57048704/answer/151446405
#include<iostream>
using namespace std;
class A{};
A fun()
{
A a;
return std::move(a);
}
int main()
{
auto f = fun();
return 0;
}
- URVO(unnamed return value optimization):函数的所有执行路径都返回同一个类型的匿名变量,会执行copy elision
- NRVO(Named Return Value Optimization):函数的所有路径都返回同一个非匿名变量,会执行copy elision
Never apply std::move or std::forward to local objects if they would otherwise be eligible for the return value optimization.
面向对象
0.面向对象的基础
- 继承
- 多态
- 封装
1.如何实现多态?
什么是多态:
一个类的实例的相同方法,在不同的情况下有不同的表现形式。
编译时多态和运行时多态:
-
编译时多态依赖的是模板技术
-
template <typename T> void animalShout(T & t) { t.shout(); //只要T实现shout方法就行 }
-
缺点在于编译时间长、代码不好阅读、难调试。优点就是运行速度快.
-
-
运行时多态依赖虚函数机制实现的动态绑定
动态绑定和静态绑定:
记住一句话:增加virtual关键字后,调用对应方法时候就会触发动态绑定,否则就是静态绑定。
一般简单的继承关系,而且方法都不是虚函数。那么在调用的这些函数的时候,只会触发静态绑定。
#include<iostream>
using namespace std;
class bird{
public:
void fly(){cout<<"bird fly"<<endl;};//鸟会飞
};
class penguin: public bird{
public:
void swim();//企鹅会游泳
void fly(){cout<<"penguin can not fly"<<endl;}
};
void func1(bird&bd){
bd.fly();
}
void func2(bird*pb){
pb->fly();
}
int main(){
bird bd;
penguin pg;
bd.fly();//bird::fly()
func1(bd);//bird::fly()
func2(&bd);//bird::fly()
//静态绑定,因为func的参数是bird。虽然pg是bird的派生类,但是fly并不是虚函数
pg.fly();//penguin::fly()
func1(pg);//bird::fly()
func2(&pg);//bird::fly()
return 0;
}
虚函数如何实现:
当编译器发现类中有虚函数时,会创建⼀张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在对象中增 加⼀个指针 vptr
,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存⼀个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。
2.创建一个类,编译器默认提供哪些方法?
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符
3.this指针
this
:是个指针,所以通过->
可以访问对象的成员函数或是方法
return this就是表示返回指向这个对象的地址
*this
:表示解引用,获取了当前这个对象! 使用对象的成员变量(*this).buf_;
return *this返回当前对象的本身或者克隆(返回类型为A则是克隆;为&A,则是本身,也就是引用)。
this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值
class Entity{
public:
Entity(int x ,int y){
//x = x; //这个时候形参和成员变量同名了,这就很难受了
this->x = x;
this->y = y;
}
public:
void print(){
//std::cout<<this->x<<" "<<this->y<<std::endl
std::cout<<x<<" "<<y<<std::endl;
}
private:
int x;
int y;
};
int main(){
Entity e(1,2);
e.func1(); //调用时,其实时这也的e.func1(&e),e被赋值给了this指针
}
4.为什么构造函数不能定义成虚函数?
因为虚函数的调用是依赖于虚函数表的,而指向虚函数表的这个指针又需要在构造函数中进行初始化,所以无法将构造函数定义成虚函数
5.为什么基类的析构函数需要定义成虚的?
回到静态绑定的问题上,如果基类的析构函数不是虚函数,那么派生类在触发对象释放的时候,传入this指针的类型是派生类,这会发生静态绑定,所以就只会调用本身的一个析构函数,如果基类持有一些指针、fd之类的资源,就会造成资源泄露或是内存泄露。
如果是虚函数,则发生动态绑定,首先调用指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数(编译器规定的,没有为什么。)
6.什么时候调用移动构造?
同拷贝构造、赋值操作符一样,移动操作也是在合适的时候被编译器调用,一般情况下:
- 函数通过值返回结果
- 对象被赋值且右侧是一个临时对象
- 对象被使用一个临时对象进行初始化
有时候源对象并不是一个临时对象,也不在上述的场景当中,那么想要显示触发移动操作,可以借助std::move操作。std:move会把当前对象直接标记为可移动的,当进行赋值操作时,就会触发移动操作。
7.什么时候调用拷贝构造函数?
-
一个对象作为函数的返回值,以值传递的方式从函数返回;
-
string print(string str){ return str; }
-
-
一个对象作为函数的参数,以值传递的方式作为入参;
-
void print(string str){ return str; }
-
-
使用已有的对象去初始化新的对象
-
std::vector<int> v1(10,1); std::vector<int> v2(v1);
-
8.如何实现重载?
- 函数名相同
- 参数个数不同
- 参数类型不同
- 如果仅仅只有返回值不同,是无法实现重载的
9.多重继承?
主要是一个二义性问题
// Base
// BaseA BaseB
// Drived
void Derived::display(){
//Drived调用Base的方法时,会出现二义性,编译器不知道是哪个方法。
}
解决方案:
- BaseA继承Base时候使用
virtual
继承 - Drived使用Base的方式时,指定一个类
d.BaseA::fun()
,但是这个做法不太好看。
10.类带有指针成员变量?
c++容器都是值语义,将元素存入容器中时,会触发拷贝构造函数调用,将元素复制了一份插入容器中。所以如果元素中含有指针成员,一定要实现一个拷贝构造函数和赋值运算符,申请新的内存后再赋值。总结:
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
class test {
public:
test(int *a) :a(a) {}
~test() {
printf("sss\n");
delete a;
a = nullptr;
}
test(const test &t) {
this->a = new int();
*this->a = *t.a;
}
test& operator=(const test& t){
if(this != &t){
if(a !=nullptr){
delete a;
a = nullptr;
}
this->a = new int();
*this->a = *t.a;
}
return *this;
}
public:
void print() {
printf("%d\n", *a);
}
public:
int *a;
};
void vector_test() {
mystl::vector<test> vt;
int *a = new int(1);
test t(a);
vt.push_back(t); //触发拷贝构造函数
vt.pop_back();
t.print();
}
11.拷贝构造函数的实现?
test& operator=(const test& t){
if(this != &t){ //地址判断
if(a !=nullptr){
delete a;
a = nullptr;
}
this->a = new int();
*this->a = *t.a;
}
return *this;
}
12.初始化列表与构造函数的区别?
编译器总是确保所有成员对象在构造函数体执行之前初始化,所以类类型的数据成员对象,在进入构造函数体之前已经完成构造。
其实我们可以这样说,初始化列表是成员变量定义的地方,也就是为其开辟空间的地方。而类里面只是对变量的声明。
使用初始化列表的好处:
- 少调用一次默认构造函数。所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
什么场景下必须使用初始化列表:
-
const类型的成员变量。(否则函数体内赋值会提示,表达式必须是可以修改的左值)
-
引用类型
-
没有默认构造函数的类
-
class Test1 { public: Test1(int a) :i(a) {} int i; }; class Test2 { public: Test1 test1; //Test2(Test1 &t1) :test1(t1) {} //这个写法ok Test2(Test1 &t1) { //提示报错:Test1没有默认的构造函数 test1 = t1; } };
-
13.一个对象访问普通成员函数和虚函数哪个更快?
普通函数。
因为普通函数的地址在编译阶段就确定了,而虚函数是一个动态绑定的过程,在调用时,需要先通过vptr指针查找虚函数表,然后再调用。
STL
0.stl的理解?
首先一点就是stl只是容器模板,是标准库中的一部分。所以stl的实现肯定也要依赖std中的其他组件哇!而std里面有些组件的实现其实是通过编译器提供的支持,c++本身自己是没法做到的… 就是很多模板最后都是转发到一堆无法查看的代码上去的。
1.为什么迭代器5种类型使用对象来表示 ,不用1,2,3,4,5?
首先迭代器的五个类型存在继承关系,因此,我可能只需要提供input_iterator和random_access_iterator 版本的函数就行了,当我萃取出迭代器类型farward_iterator时,会自动匹配到input_iterator,这样子重载就很方便。
2.你是如何理解迭代器的?和指针相比有什么区别?
迭代器是一个类模板,存储容器信息的一个对象。每个容器都会实现针对容器本身的container_iterator class。与容器类进行组合,作为成员变量出现。迭代器最重要的工作是对 **operator*和operator->**进行重载,这样通过*iter就能得到迭代器所指对象的值,通过iter->能调用相关方法。
3.map、set、multimap、multiset、unorderedXXX?
红黑树系列:
采用红黑树,因此对应有顺序要求的情况下,使用map会更好一些。
-
multiXXX和普通的容器区别就是让key能够重复。
-
map
- 元素以key+val的值存在(pair)
- 自动排序
- 不允许key重复
-
set
- 元素以key存在,在set中key就是value,value同时也是key
- 主要用于自动去重排序
哈希表系列:
主要在于unorerded系统容器底层实现采用的是hash表,因此查找速度非常块,适用于一些需要查找的场景。
优缺点对比:
两种容器的比较其实就是红黑树和哈希表的对比:
- todo
5.迭代器失效?
记住一点:erase释放后返回的下一个迭代器节点。
迭代器不能看作是指针,迭代器是和容器的元素进行绑定的。
- 由于插入元素,导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效
- 由于删除操作,容器中元素都没有了,迭代器肯定失效了嘛。
还可以从另外三个角度看:
- 数组型结构:
- 失效:插入和删除操作,都会使得操作节点之后的元素移动位置,进而导致失效。
- 解决:
- 删除:iter = container.erase(iter); //删除返回下一个迭代器
- 插入:iter = container.insert(iter); iter++; //插入返回当前的迭代器
- 链表型结构:
- 失效:由于是非连续的结构,所以插入操作没影响。只在删除操作后,被删除节点本身迭代器失效。
- 解决:iter = container.erase(iter);
- 树型结构:
- 失效:删除操作导致本节点迭代器失效
- 解决:iter = container.erase(iter);
6.容器中存储指针和对象的区别?
容器的在删除元素时,会根据元素类型来选择是否调用析构函数:
// MyTinySTL中的一段代码
template <class Ty>
void destroy(Ty* pointer)
{
//std::is_trivially_destructible检测类型是否可破坏
//这玩意也就是说针对自定义类会需要调用析构.
destroy_one(pointer, std::is_trivially_destructible<Ty>{});
}
template <class Ty>
void destroy_one(Ty*, std::true_type) {}
template <class Ty>
void destroy_one(Ty* pointer, std::false_type)
{
if (pointer != nullptr)
{
pointer->~Ty();
}
}
存放对象:
当容器释放元素的时候会调用destory操作,destory判断类型不是指针类型,就要调用这个类的析构函数。
存放指针: 当容器释放元素的时候会调用destory操作,destory判断类型是指针类型,就啥也不做了,所以需要我们手动对元素进行释放操作。
int main(int argc, char *argv[]) {
std::list<Image *> imgs;
for (size_t i = 0; i < 10; i++) // 添加10张图片
{
imgs.push_back(new Image(100, 100));
}
while (true) {
imgs.push_back(new Image(100, 100));
Image *pImg = imgs.front();// 取出队首图片
consumer(pImg);// 消费队首图片
//==========
delete pImg;
pImg = nullptr;
//==========
imgs.pop_front();
Sleep(10);
}
return 0;
}
7.容器存储元素中含有指针成员怎么处理?
c++容器都是值语义,将元素存入容器中时,会触发拷贝构造函数调用,将元素复制了一份插入容器中。所以如果元素中含有指针成员,一定要实现一个拷贝构造函数和赋值运算符,申请新的内存后再赋值。需要实现:
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
8.vector容器释放内存?
在容器vector中,其内存占用的空间是只增不减的,比如说首先分配了10,000个字节,然后erase掉后面9,999个,则虽然有效元素只有一个,但是内存占用仍为10,000个。所有内存空间在vector析构时回收。
一般,我们都会通过vector中成员函数clear进行一些清除操作,但它清除的是所有的元素,使vector的大小减少至0,却不能减小vector占用的内存。
要避免vector持有它不再需要的内存,这就需要一种方法来使得它从曾经的容量减少至它现在需要的容量,这样减少容量的方法被称为“收缩到合适(shrink to fit)”。如果做到“收缩到合适”呢,就要全仰仗“Swap大侠”啦,即通过如下代码进行释放过剩的容量:
std::vector<T>().swap(X)
// 作用相当于:
{
std::vector<T> temp(X);
temp.swap(X); //此时temp获取了所有内存,然后局部对象被析构所以释放了内存。
}
9.string容器扩容问题?
string底层并不是一个数组,而是通过operator new申请的一块内存。
string容器重载了operator+
操作符,当拼接的字符串的大小比string容器预留的空间更大时,就会触发重新申请内存并且把之前的数据拷贝过去的操作。这样子来看,挺消耗性能的。好在之前的数据会被释放掉。
10.stl的内存管理?
stl采用了allocator来对内存进行管理。将new这个关键字的操作划分成了三部分:
- allocate:封装operator new申请内存
- deallocate:封装operator delete释放内存
- construct:封装placement new,在operator申请的内存上进行对象构造
- destory:调用类类型的析构函数
...