C++基础

C++基础

面试题1
const
Linux进程分配内存的两种方式–brk() 和mmap()
C++类内存模型

1.回调函数

(1)作用:
上层传入回调函数指针,实现下层调度上层的函数。
回调函数的一个好处是,可以通过回调函数,调用不同接口。
比如接收数据,不同数据需要不同的解析函数,但是接收数据的流程是相同的,
这样 使用回调函数,在接收数据的时候就可以不用判断,而是自动使用不同的解析函数。
总之 使用回调函数,可以降低耦合,实现多样性。

2.C++11新特性,C++14新特性

编译要加上“-std=c++11”: g++ test.cpp -o test.out -std=c++11
(1)类型推断
auto i = 1;
auto k = i;
auto str = “abcedf”;
auto作为函数返回值时,只能用于定义函数,不能用于声明函数
(2)nullptr
nullptr是为了解决原来C++标准中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0。
(3)基于范围的for循环
简化了常见的循环,对数组或容器类等的每个元素执行相同的操作
int a[] = {1,2,3,4,5};
for(auto x: a) cout<<x<<endl;
(4)final关键字
•override,表示函数应当重写基类中的虚函数(VS2010目前支持)。
•final,阻止派生类重写这个虚函数(VS2010目前不支持)。

struct Object{
    virtual void fun() = 0;
};
struct Base : public Object {
    void fun() final;   // 声明为final
};
struct Derived : public Base {
    void fun();     // 无法通过编译
};

(5)STL
【1】array
std::array相对于数组,增加了迭代器等函数
【2】unordered_map
unordered_map与std::map用法基本差不多,但STL在内部实现上有很大不同,std::map使用的数据结构为二叉树,而std::unordered_map内部是哈希表的实现方式,哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。

(6)智能指针
[1] shared_ptr

3.类型转换
4.多态

多态
泛型编程:模板(函数模板,类模板)
所谓泛型编程就是以独立于任何特定类型的方式编写代码

5.虚继承

B C虚继承A,D public继承 B C ,有A *a = new D,a->fun(),fun是虚函数,并且B C都重写了,怎么保证a调用的是B重写的虚函数
虚继承

6.关于const 几种用法

(1)const 变量:不让改变量
(2)const 指针:两种用法,
“const int *p;” 不让改内容;
“int * const p;” 不让改地址
“const int * const p;” 内容和地址都不让改
(3)const 修饰函数返回值(最前面):返回值不能改变(待详细)
(4)const 成员函数: 作用是不让该函数改对象的成员变量
例如: void func1(void) const; // 定义和声明都需要加 const
说明原因:
一个类对象调用一个普通成员函数
A a; a.func(); 可看作 func(&a); func可看作 void func( A * const this);
所以说普通的成员函数是可以修改对象的成员变量的值的,为了不让这函数修改成员变量,可变成void func( const A * const this);因此C++在设计时,将第一个const放到函数的 末尾当做这种写法
注意:
[1] const对象不能调用非const成员函数,因为怕被改成员变量的值,即const对象只能调用const成员函数
[2] 非const对象(正常对象)都可以调用
(5)没有const 引用,编译不过

7.struct 和 class

(1)struct和class的异同
[1] class默认属性(成员变量和函数,继承)是 private,struct是public
[2] struct是为了在C++中兼容C
[3] 使用习惯上 class是类,有函数操作和数据,struct是数据
(2)class
[1]对于空类 class A{}; sizeof(A) 等于1,因为编译器为了标记他的实例。若此时有一个int变量,则是4.
[2]sizeof(类A)相当于sizeof(A的对象)
[3]class A{ virtuall void test(); }; ,sizeof(A) 等于 地址长度 4(32位系统,64位是8)
(3)struct
[1] 求下面的结果,并说出为什么
#include <stdio.h>
typedef struct Point{
int x;
int y;
int z;
} POINT;
int main(){
POINT *ptr = NULL;
int offset = (int)(&(ptr)->z);
printf("%d \n",offset);
return 0;
}
答:8,ptr = NULL是对 ptr做初始化赋值为0,“&(ptr)->z”是在 0 这个地址上做偏移
[2] C 与 C++ 中 struct的区别
C中的struct是数据的封装,不可隐藏数据,无函数,不能直接用结构体名来声明变量
C++中的struct等同于class,有函数,有权限,可直接用结构体名来声明变量。

8.迭代器的插入删除操作

(1)以下操作有什么问题,应该怎么改
迭代器的插入删除
解决问题的关键是:迭代器的插入和删除操作会导致迭代器失效
错误点:第二个for循环中,如何改:
a. 判断条件的 ivEnd改为 intList.end()
b. insert和erase会返回新的迭代器,改为 iter = intList.insert(…) , iter = intList.erase(…)

9.成员变量

(1)成员变量初始化顺序按照声明的顺序被初始化,而不是初始化列表的顺序
该代码有什么问题
能运行,但是输出结果不能按照预期的那样
因为系统首先初始化 n1 然后 n2,但是n1依赖于n2,但是n2这时是随机值
n2倒是能正常初始化,输出结果是0
(2)静态成员变量必须在类外初始化

10.类与类的关系

(1)继承
分为父类(Base)和子类(Drived),父类有的子类都可以有
在这里插入图片描述

(2)组合
一个类中有包含另外一个类 (类中的成员变量之一是类),**是包含一个对象,而不是包含一个指针,*如果你组合了这个类,那么你就将拥有你包含的类的全部功能
class queue {
protected:
std::deque c;
public:

};
在这里插入图片描述
在这里插入图片描述
(3)聚合
也就是委托关系
一个类中包含另一个类的指针,你也同样拥有被包含类的全部功能。跟组合很像。
class String {//handle
public:

private:
StringRep
rep; // pimpl
};
在这里插入图片描述

11.引用

(1)使用方式
int a = 10;
int b = 21;
int &ref = a; // 定义的时候初始化
不能中途改变引用:“错的:ref = &b”,ref = b这样是赋值。

(2)引用的原理
引用的本质就是所引用对象的地址。(编译器来实现)
即引用就是通过指针来实现的,所以引用占内存大小为指针大小,sizeof(&引用)== sizeof(指针),sizeof(ref)== sizeof(指向对象的大小)

(3)关于 “const 引用”
[1] const 在 &的前面,表示不允许修改引用对象的值,如
const int &ref = a;
[2] const 在 &后面,编译报错

(4)指针和引用的区别
[1]. 引用只能在定义时初始化一次,之后不能改变指向其它变量(从一而终);指针变量的值可变。
[2]. 引用必须指向有效的变量,指针可以为空。
[3]. sizeof指针对象和引用对象的意义不一样。sizeof引用得到的是所指向的变量的大小,而sizeof指针是对象地址的大小。
[4]. 指针和引用自增(++)自减(–)意义不一样。
[5]. 相对而言,引用比指针更安全。

(5)何时用引用好,何时用指针好?
[1] 当指向的对象不改变时,使用引用
[2] 指向的对象可能会改变,或为空时,使用指针
[3] 尽可能使用引用,这样更安全和便捷。

12.内存区

(1)Linux C++内存分布图
内存分布图
(2)5大分区
[1].栈区(stack)局部变量,形参,返回值,由编译器自动分配与释放,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作,效率高,支持的数据有限。

[2].堆区(heap):由malloc等函数库提供,对应free释放,容量要远远大于栈。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
注意
a.栈区和堆区的空间都有限,不能分配很大的空间,否则会栈越界,导致段错误
b.栈不会产生内存碎片,堆会,频繁调用malloc、free会产生很多内存碎片

[3]. 自由存储区(free store):由new分配,对应delete。是C++基于new操作符的一个抽象概念,可以是堆 或 静态存储区,取决于operator new如何操作。
new可以不分配内存:如 new (place_addr) type:place_addr是一块地址,new只是在这块地址上进行初始化操作。

[4].全局/静态存储区全局变量静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++中没有这个区分了,他们共同占用同一块内存区。

[5].常量区:存放的是常量,字符串,是一块特殊的存储区,不允许修改(当然,你要通过非正当手段也可以修改)

13.new/delete 和 malloc/free 的区别

(1)malloc/free是标准库函数,new/delete是C++运算符
(2)malloc失败返回NULL,需要判断NULL,new失败抛异常,可以try…catch
(3)new/delete会调用构造、析构函数,malloc/free不会
(4)new运算不需要进行强制类型转换,使用简单方便
new和malloc的区别

14.普通new、placement new

(1)普通的new操作
当我们使用new/delete时,其实执行的是全局的::operator new和::operator delete。
class A{…};
A* pa = new A;
delete pa;
[1] new包含两阶段的操作
先调用 ::operator new分配内存 =>然后调用A::A() 构造函数初始化对象内容
[2] delete也分两部分的操作
先调用A::~A()将对象析构 => 然后调用::operator delete释放内存

(2)placement new(定位放置new) 操作
[1] 利用已有空间来生成对象,这个过程会自动调用构造函数,但是由于对象的空间不会自动释放(对象实际上是借用别人的空间),所以必须显示的调用类的析构函数。
A* p=new (mem) A;// 会自动调用类A的构造函数
p->~A();// 显示调用析构函数
// delete p; // No!!!
[2] 使用场景及好处
a.可以灵活的在已有的内存(栈、堆)构建对象,速度快
b.可提前申请好一块大的空间来使用,也防止产生内存碎片,以及防止系统中存在大量内存碎片时,申请内存失败的情况

(3) 重载 new / delete 操作符

#include <iostream>
#include <stdlib.h>
using namespace std;

class Test
{
public:
    Test(int n) : n_(n) { cout << "构造函数 : Test(int n) : n_(n), n="<< n << endl; }
    Test(const Test& other) { cout << "Test(const Test& other)" << endl; }
    ~Test() { cout << "~Test()" << endl; }
    void *operator new(size_t size)//普通情况调用,标注为0
    {
        cout << "1.void* operator new(size_t size); size="<< size << endl;
        void* p = malloc(size);
        return p;
    }
    void operator delete(void* p)
    {
        cout << "1.void operator delete(void* p)" << endl;
        free(p);
    }
    void operator delete(void* p, size_t size)//如果上面那个和这个delete都有,那么调用上面的。
    { cout << "2.void operator delete(void* p, size_t size)" << endl; free(p); }

    void* operator new(size_t size, void* p)//另一种情况调用,标注为2
    { cout << "2.placement new, size="<< size << endl; return p; }

    int n_;
};

void* operator new[](size_t size)
{ cout << "3.全局 operator new[], size="<< size << endl; void * p = malloc(size); return p; }

void operator delete[](void * p) { cout << "3.全局 operator delete[]" << endl; free(p); }

void * operator new(size_t size)
{ cout << "4.全局 operator new, size="<< size << endl; void * p = malloc(size); return p; }

void operator delete(void * p) { cout << "4.全局 operator delete" << endl; free(p); }

int main(void)
{
    //第一个知识点
    Test* p1 = new Test(100);    // new operator = operator new + 构造函数的调用 调用标注0
    delete p1;                  //delete = 析构函数的调用+operator delete

    //第二个知识点
    char* str = new char[100];
    delete[] str;

    char * str2 = new char;
    delete str2;

    //第三个知识点
    char chunk[10];
    Test* p2 = new (chunk) Test(200);    //operator new(size_t, void *_Where)+构造函数的调用
                                         // placement new,不分配内存+ 构造函数的调用
    cout << p2->n_ << endl;
    // delete p2; // No!!!
    p2->~Test();                        // 显式调用析构函数,因为没有分配内存,所以直接调用析构函数
    return 0;
}
15.结构体内存对齐

参考1
(1)结构体对齐方式:4字节对齐
(2)32位系统的对齐方式
[1] 为什么要对齐?
因为在32位操作系统(虽然64位操作系统,但是为了保证兼容性,编程仍然主要考量32位)中,数据总线是32位,地址总线是32位。
地址总线是32位,意味着寻址空间是按4递增的;数据总线32位意味着一次可读写4字节。
对于这个结构体:

struct stu1  {   
   char c1;   
   int i;  
   char c2;  
} 

1
不对齐就意味着,当我们执行stu1.i 时,需要读取内存两次。而对齐后,就只需要读取一次,众所周知,I/O操作是很耗时的,编译器做出对齐的选择也就好理解。

[2] 为什么不完全按照4字节对齐的?
既然对齐可以避免上述的问题,为什么不将所有存储小于4byte的数据类型(char, short等)统统按4byte对齐呢?
比如:

struct stu2  {   
   char c;   
   short s;
   int i; 
   double d; 
}

2
为什么编译器采用B方案,而不采用A方案?
还是因为32数据线一次读取4个字节,
采用方案A,读取stu2.c,或者stu2.s,要一次读取4个byte,再舍弃无关内存的数据。
采用方案B,读取stu2.c,或者stu2.s,也是要一次读取4个byte,再舍弃无关内存中的数据。
同样的I/O操作,相比之下,明显方案B更节省内存

补充: 如上图中,所示8字节的数据类型,比如double, long long,必须要读取两次内存。

注意:嵌套结构体是按照展开后的方式来对齐,而不是把内嵌的结构体作为一个整体

(3)64位系统的对齐方式
先来看看以下C代码的输出

#include <stdio.h>
typedef struct _s1{
	char a;
	int b;
	double c;
} S1;
typedef struct _s2{
	char a;
	double c;
	int b;
} S2;
typedef struct _s3{
	double c;
	char a;
	int b;
} S3;

int main() {
	S1 s01;  S2 s02;  S3 s03;
	printf("s01:%ld, s01.a:%ld, s01.b:%ld, s01.c:%ld \n",sizeof(s01), sizeof(s01.a), sizeof(s01.b), sizeof(s01.c));
	printf("s02:%ld, s02.a:%ld, s02.b:%ld, s02.c:%ld \n",sizeof(s02), sizeof(s02.a), sizeof(s02.b), sizeof(s02.c));
	printf("s03:%ld, s03.a:%ld, s03.b:%ld, s03.c:%ld \n",sizeof(s03), sizeof(s03.a), sizeof(s03.b), sizeof(s03.c));

	return 0;
}

输出结果:

s01:16, s01.a:1, s01.b:4, s01.c:8 
s02:24, s02.a:1, s02.b:4, s02.c:8 
s03:16, s03.a:1, s03.b:4, s03.c:8 

这里首先普及一个知识:
操作系统分32位和64位两种,电脑cpu也有32位和64位之分,通常64位cpu同时支持32位和64位两种系统的安装,32位cpu只能装32位系统,不支持安装64位系统,目前已经很少有32位的CPU了。
开始正题:
对于64位系统,一次操作8个字节,因此,以上的结构体内存分布为:
在这里插入图片描述
因此,在定义结构体时,要适当的调整顺序,以便尽量的节省内存

16.大端小端

(1)大小端是什么
大端Big Endian :高位字节 放在内存的 低地址端(颠倒,帮助记忆)
小端Little Endian:低位字节 放在内存的 低地址端
在这里插入图片描述
从例子中可以看出小端比较符合人的思维,而大端则看上去非常直观

(2)谁在使用大小端
在计算机界最先出现的是大端,权值最大的位放在前面,这与人的正常思维一致,但是在类型转型(如int 转换成short int 或char)时比较麻烦。
后来一些聪明的工程师发现采用小端模式可以非常完美的解决这一问题,转换时不需要计算转换后的地址偏移,直接拿之前的地址读取指定的字节数就可以了,这个时候PC机才刚刚起步,所以小端模式大量的应用于之后的芯片,而早期的计算机多是服务器,为了满足软件的兼容性,不得不沿用以前的方案。

常见CPU的字节序:
大端 : PowerPC、IBM、Sun
小端 : x86、DEC
大小端可选:ARM
PowerPC主导网络市场,所以大多数网络用大端
Pentium主导PC市场,因此多数用于个人机的外设都采用小端模式

网络字节序(Network Order):TCP/IP各层协议将字节序定义为Big Endian,因此TCP/IP协议中使用的字节序通常称之为网络字节序。

主机字节序(Host Order):整数在内存中保存的顺序,它遵循Little Endian规则(不一定,要看主机的CPU架构),。所以当两台主机之间要通过TCP/IP协议进行通信的时候就需要调用相应的函数进行主机序列(Little Endian)和网络序(Big Endian)的转换

(3)代码验证

#include <stdio.h>
int main() {
	int num = 0x12345678;
	char *p = (char*)(&num);

	printf("p  :%p  :%02x \n",p,*p);
	printf("p+1:%p  :%02x \n",p+1,*(p+1));
	printf("p+2:%p  :%02x \n",p+2,*(p+2));
	printf("p+3:%p  :%02x \n",p+3,*(p+3));
	return 0;
}

输出结果:

p  :0x7fff6c1d4644  :78 
p+1:0x7fff6c1d4645  :56 
p+2:0x7fff6c1d4646  :34 
p+3:0x7fff6c1d4647  :12 
18.C++类型转换
19.智能指针
20.虚函数

(1)
(2)
(3)虚函数表和虚函数指针是什么时候确定的
虚函数表是编译的时候确定的
虚函数指针是运行阶段确定的?

21.左值 和 右值
22.用C如何实现C++类的封装
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值