C/C++重点八股文

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    1.C/C++关键字

    1.1 static静态变量

    在C中关键字static是静态变量

    • 静态变量只会初始化一次然后在这函数被调用过程中值不变。
    • 在文件内定义静态变量函数外作用域是当前文件该变量可以被文件内所有函数访问不能被其他文件函数访问。为本地的全局变量只初始化一次。

    在C++中类内数据成员可以定义为static

    • 对于非静态数据成员每个对象有一个副本。而静态数据成员是类的成员只存在一个副本被所有对象共享。
    • 静态成员变量没有实例化对象也可以使用“类名静态成员变量”
    • 静态成员变量初始化在类外但是private和protected修饰的静态成员不能类外访问。
    class Stu
    {
    public:
    	static int age;
    private:
    	static int height;
    };
    //初始化静态成员变量
    int Stu::age = 19;
    int Stu::height = 180;
    
    int main()
    {
    	cout<<Stu::age<<endl;//输出19
    	cout<<Stu::height<<endl;//错误的私有无法访问。
    	Stu s;
    	cout<<s::age<<endl;//输出19
    	cout<<s::height<<endl;//错误的私有无法访问。
    	return 0;
    }
    
    • 在类中static修饰的函数是静态成员函数。静态成员函数一样属于类不属于对象被对象共享。静态成员函数没有this指针不能访问非静态的函数和变量只能访问静态的。

    与全局变量相比静态数据成员的优势

    • 全局变量作用域是整个工程而static作用域是当前文件避免命名冲突
    • 静态数据成员可以是private成员而全局变量不能实现信息隐藏

    为什么静态成员变量不能在类内初始化

    因为类的声明可能会在多处引用每次引用都会初始化一次分配一次空间。这和静态变量只能初始化一次只有一个副本冲突因此静态成员变量只能类外初始化

    为什么static静态变量只能初始化一次

    所有变量都只初始化一次。但是静态变量在全局区静态区而自动变量在栈区。静态变量生命周期和程序一样只创建初始化一次就一直存在不会销毁。而自动变量生命周期和函数一样函数调用就进行创建初始化函数结束就销毁所以每一次调用函数就初始化一次。

    在头文件中定义静态变量是否可行

    不可行在头文件中定义的一个static变量对于包含该头文件的所有源文件实质上在每个源文件内定义了一个同名的static变量。造成资源浪费可能引起bug

    静态变量什么时候初始化

    • 初始化只有一次但是可以多次赋值在主程序之前编译器已经为其分配好了内存。

    • 静态局部变量和全局变量一样数据都存放在全局区域所以在主程序之前编译器已经为其分配好了内存但在C和C++中静态局部变量的初始化节点又有点不太一样。

    • 在C中初始化发生在代码执行之前编译阶段分配好内存之后就会进行初始化所以我们看到在C语言中无法使用变量对静态局部变量进行初始化在程序运行结束变量所处的全局内存会被全部回收。

    • 而在C++中初始化时在执行相关代码时才会进行初始化C++标准定为全局或静态对象是有首次用到时才会进行构造并通过atexit()来管理。在程序结束按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

    1.2 const的作用

    常量类型也称为const类型使用const修饰变量或者对象

    在C中const的作用为

    • 定义变量局部或者全局为常量
    const int a = 10; //常量定义时必须初始化
    
    • 修饰函数的参数函数体内不能修改这个参数的值
    • 修饰函数的返回值
      • const修饰的返回值类型为指针返回的指针不能被修改而且只能符给被const修饰的指针
        const char* GetString()
        {
        	//...
        }
        
        int main()
        {
        	char *str = GetString();//错误str没被const修饰
        	const char *str = GetString();//正确
        }
        
        
      • const修饰的返回值类型为引用那么函数调用表达式不能做左值函数不能被赋值
        const int & add(int &a , int &b)
        {
        	//..
        }
        int main()
        {
        	add(a,b) = 4;//错误const修饰add的返回引用不能做左值
        }
        
      • const修饰的返回值类型为普通变量由于返回是普通临时变量const修饰没意义。

    在c++中const还有作用为

    • const修饰类内的数据成员。表示这个数据成员在某个对象的生命周期是常量不同对象的值可以不一样因此const成员函数不能在类内初始化。
    • const修饰类内的成员函数。那么这个函数就不能修改对象的成员变量

    const的优点

    1. 进行类型检查使编译器对处理内容有更多了解。

    2. 避免意义模糊的数字出现类似宏定义方便对参数进行修改。

    3. 保护被修饰的内容防止被意外修改

    4. 为函数重载提供参考

      class A
      {
      	void f(int i){...} //非const对象调用
      	void f(int i) const {...}//const对象调用
      }
      

    5.节省内存
    6.提高程序效率编译器不为普通const常量分配存储空间而保存在符号表中。称为一个编译期间的常量没有存储和读内存的操作

    什么时候使用const

    • 修饰一般常量

    • 修饰对象

    • 修饰常指针

      const int *p;
      int const *p;
      int *const p;
      const int *const p;
      
    • 修饰常引用

    • 修饰函数的参数

    • 修饰函数返回值

    • 修饰类的成员函数

    • 修饰另一文件中引用的变量

      extern const int j;
      

    const和指针常量指针、指针常量

    • 常量指针const 修饰常量const在*的左边

      const int *p = &a; // const修饰int,指针的指向可以修改但是指针指向的值不能改
      int const *p;//同上
      p = &b;//正确
      *p = 10;//错误
      
    • 指针常量const修饰指针const在*的右边

      int *const p = &a;//const修饰指针指针的指向不可以改但是指针指向的值可以改
      *p = 10;//正确
      p = &b;//错误
      
    • const都修饰指针和常量指针和常量都不能修改

      const int *const p;
      int const *const p;
      

    顶层const和底层const

    • 顶层const常量指针指的是const修饰的变量本身是一个常量无法修改指的是指针就是 * 号的右边
    • 底层const指针常量指的是const修饰的变量所指向的对象是一个常量指的是所指变量就是 * 号的左边

    const和static的作用

    static

    • 不考虑类的情况
      • 隐藏。所有不加static的全局变量和函数具有全局可见性可以在其他文件中使用加了之后只能在该文件所在的编译模块中使用
      • 默认初始化为0包括未初始化的全局静态变量与局部静态变量都存在全局未初始化区
      • 静态变量在函数内定义始终存在且只进行一次初始化具有记忆性其作用范围与局部变量相同函数退出后仍然存在但不能使用
    • 考虑类的情况
      • static成员变量只与类关联不与类的对象关联。定义时要分配空间不能在类声明中初始化必须在类定义体外部初始化初始化时不需要标示为static可以被非static成员函数任意访问。
      • static成员函数不具有this指针无法访问类对象的非static成员变量和非static成员函数不能被声明为const、虚函数和volatile可以被非static成员函数任意访问

    const

    • 不考虑类的情况

      • const常量在定义时必须初始化之后无法更改

      • const形参可以接收const和非const类型的实参例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}

    • 考虑类的情况

      • const成员变量不能在类定义外部初始化只能通过构造函数初始化列表进行初始化并且必须有构造函数不同类对其const数据成员的值可以不同所以不能在类中声明时初始化
      • const成员函数const对象不可以调用非const成员函数非const对象都可以调用不可以改变非mutable用该关键字声明的变量可以在const成员函数中被修改数据的值

    补充一点const相关const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的因为即使被多个文件包含链接性都是内部的不会出现符号冲突

    1.3 switch语句中case结尾是否必须加break

    **一般必须在case结尾加break。**因为通过switch确认入口点一直往下执行直到遇见break。否则会执行完这个case后执行后面的casedefault也会执行。 注switchcc可以是int、long、char等但是不能是float

    1.4 volatile 的作用

    volatile 关键字是一种类型修饰符用它声明的类型变量表示可以被某些编译器未知的因素更改比如操作系统、硬件或者其它线程等。

    • 编译器不再进行优化从而可以提供对特殊地址的稳定访问。
    • 系统总是重新从它所在的内存读取数据不会利用cache中原有的数值。
    • 用于多线程被多个任务共享的变量或者并行设备的硬件寄存器

    1.5 断言ASSERT()是什么

    **是一个调试程序使用的宏。**定义在<assert.h>中用于判断是否出现非法数据。括号内的值 为false(0)程序报错终止运行。

    ASSERT(n != 0);// n为0的时候程序报错
    k = 10/n;
    

    ASSERT()在Debug中有在Release中被忽略。 ASSERT()是宏assert()是ANSCI标准中的函数但是影响程序性能。

    1.6 枚举变量的值计算

    #include<stdio.h>
    int main()
    {
    	enum {a,b=5,c,d=4,e};
    	printf("%d %d %d %d %d",a,b,c,d,e); 
    	return 0;
    }
    

    输出为 0 5 6 4 5

    1.7 字符串存储方式

    1. 字符串存储在栈中
    char str1[] = "abc";
    char str2[] = "abc";
    
    1. 字符串存储在常量区
    char *str3 = "abc";
    char *str4 = "abc";
    
    1. 字符串存储在堆中
    char *str5 = char*malloc(4);
    strcpy(str5,"abc");
    char *str6 = char*malloc(4);
    strcpy(str6,"abc");
    
    1. 字符串是否相等
    • str1 = str2 str1和str2是两个字符串的首地址。
    • str3 == srt4 str3和str4是常量的地址同样字符串在常量区只存在一份。
    • str5 = str6 str5 和str6是指向堆的地址。
      在这里插入图片描述

    1.8 程序内存分区

    内存高地址栈区
    堆区
    全局/静态区 .bss段 .date段
    常量区
    内存低地址代码区

    在这里插入图片描述

    1. 栈区stack
    • 临时创建的局部变量存放在栈区。

    • 函数调用时其入口参数存放在栈区。

    • 函数返回时其返回值存放在栈区。

    • const定义的局部变量存放在栈区。

    1. 堆区heap
    • 堆区用于存放程序运行中被动态分布的内存段可增可减。

    • malloc函数分布的内存必须用free进行内存释放否则会造成内存泄漏。

    1. 全局区静态区
    • c语言中全局区有.bss段和.data段组成可读可写。
    • C++不分bss和data
    1. .bss段
    • 未初始化的全局变量存放在.bss段。

    • 初始化为0的全局变量和初始化为0的静态变量存放在.bss段。

    • .bss段不占用可执行文件空间其内容有操作系统初始化。

    1. .data段
    • 已经初始化的全局变量存放在.data段。

    • 静态变量存放在.data段。

    • .data段占用可执行文件空间其内容有程序初始化。

    • const定义的全局变量存放在.rodata段。

    1. 常量区
    • 字符串存放在常量区。

    • 常量区的内容不可以被修改。

    1. 代码区
    • 程序执行代码二进制代码文件存放在代码区。

    1.9 *p++ 和 *p++ 的区别

    • *p++ 先完成取地址然后对指针地址进行++再取值
    • *p++先完成取值再对值进行++

    1.10 new / delete 与 malloc / free的异同

    • 相同点

      • 都可用于内存的动态申请和释放
    • 不同点

      • new / delete 是C++运算符malloc / free是C/C++语言标准库函数

      • new自动计算要分配的空间大小malloc需要手工计算

      • malloc和free返回的是void类型指针必须进行类型转换new和delete返回的是具体类型指针。

      • new是类型安全的malloc不是。例如

        int *p = new float[2]; //编译错误
        int *p = (int*)malloc(2 * sizeof(double));//编译无错误
        
      • malloc / free需要库文件支持new / delete不用

      • new是封装了malloc直接free不会报错但是这只是释放内存而不会析构对象

    1.11 new和delete是如何实现的

    • new的实现过程是首先调用名为operator new的标准库函数分配足够大的原始为类型化的内存以保存指定类型的一个对象接下来运行该类型的一个构造函数用指定初始化构造对象最后返回指向新分配并构造后的的对象的指针
    • delete的实现过程对指针指向的对象运行适当的析构函数然后通过调用名为operator delete的标准库函数释放该对象所用内存

    1.12 被free回收的内存是立即返还给操作系统吗

    不是的被free回收的内存会首先被ptmalloc使用双链表保存起来当用户下一次申请内存的时候会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并避免过多的内存碎片。

    1.13 C++中几种类型的new

    1. plain new
      言下之意就是普通的new就是我们常用的new在C++中定义如下

      void* operator new(std::size_t) throw(std::bad_alloc);
      void operator delete(void *) throw();
      

      plain new在空间分配失败的情况下抛出异常std::bad_alloc而不是返回NULL

      #include <iostream>
      #include <string>
      using namespace std;
      int main()
      {
      	try
      	{
      		char *p = new char[10e11];
      		delete p;
      	}
      	catch (const std::bad_alloc &ex)
      	{
      		cout << ex.what() << endl;
      	}
      	return 0;
      }
      //执行结果bad allocation
      
    2. nothrow new
      nothrow new在空间分配失败的情况下是不抛出异常而是返回NULL定义如下

      void * operator new(std::size_t,const std::nothrow_t&) throw();
      void operator delete(void*) throw();
      
      #include <iostream>
      #include <string>
      using namespace std;
      
      int main()
      {
      	char *p = new(nothrow) char[10e11];
      	if (p == NULL) 
      	{
      		cout << "alloc failed" << endl;
      	}
      	delete p;
      	return 0;
      }
      //运行结果alloc failed
      
    3. placement new
      这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败因为它根本不分配内存它做的唯一一件事情就是调用对象的构造函数。定义如下

      void* operator new(size_t,void*);
      void operator delete(void*,void*);
      

    使用placement new需要注意两点

    • palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组

    • placement new构造起来的对象数组要显式的调用他们的析构函数来销毁析构函数并不释放对象的内存千万不要使用delete这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

      #include <iostream>
      #include <string>
      using namespace std;
      class ADT{
      	int i;
      	int j;
      public:
      	ADT(){
      		i = 10;
      		j = 100;
      		cout << "ADT construct i=" << i << "j="<<j <<endl;
      	}
      	~ADT(){
      		cout << "ADT destruct" << endl;
      	}
      };
      int main()
      {
      	char *p = new(nothrow) char[sizeof ADT + 1];
      	if (p == NULL) {
      		cout << "alloc failed" << endl;
      	}
      	ADT *q = new(p) ADT;  //placement new:不必担心失败只要p所指对象的的空间足够ADT创建即可
      	//delete q;//错误!不能在此处调用delete q;
      	q->ADT::~ADT();//显示调用析构函数
      	delete[] p;
      	return 0;
      }
      //输出结果
      //ADT construct i=10j=100
      //ADT destruct
      

    1.14 delete p、delete [] p、allocator都有什么作用

    • delete p 为消除一个对象。

    • delete[]时数组中的元素按逆序的顺序进行销毁

    • new在内存分配上面有一些局限性new的机制是将内存分配和对象构造组合在一起同样的delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行allocator申请一部分内存不进行初始化对象只有当需要的时候才进行初始化操作。

    1.15 malloc与free的实现原理

    1、 在标准C库中提供了malloc/free函数分配释放内存这两个函数底层是由brk、mmap、munmap这些系统调用实现的;

    2、 brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中堆和栈中间称为文件映射区域的地方找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候发生缺页中断操作系统负责分配物理内存然后建立虚拟内存和物理内存之间的映射关系

    3、 malloc小于128k的内存使用brk分配内存将_edata往高地址推malloc大于128k的内存使用mmap分配内存在堆和栈之间找一块空闲内存分配brk分配的内存需要等到高地址内存释放以后才能释放而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K可由M_TRIM_THRESHOLD选项调节时执行内存紧缩操作trim。在上一个步骤free的时候发现最高地址空闲内存超过128K于是内存紧缩。

    4、 malloc是从堆里面申请内存也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时就会遍历该链表然后就寻找第一个空间大于所申请空间的堆结点然后就将该结点从空闲结点链表中删除并将该结点的空间分配给程序。

    1.16 malloc、realloc、calloc的区别

    • malloc函数

      void* malloc(unsigned int num_size);
      int *p = malloc(20*sizeof(int));申请20int类型的空间
      
    • calloc函数

      void* calloc(size_t n,size_t size);
      int *p = calloc(20, sizeof(int));
      

      省去了人为空间计算malloc申请的空间的值是随机初始化的calloc申请的空间的值是初始化为0的

    • realloc函数

      void realloc(void *p, size_t new_size);
      

      给动态分配的空间分配额外的空间用于扩充容量。

    1.17 exit()和return 的区别

    • return是语言级的标志调用堆栈的返回。是从当前函数的返回main中return的退出程序
    • exit是函数强行退出程序并返回值给系统
    • return实现函数逻辑函数的输出。exit只用来退出。

    1.18 extern和export的作用

    变量的声明有两种情况

    1. 一种是需要建立存储空间的。例如int a 在定义的时候就已经建立了存储空间。

    2. 另一种是不需要建立存储空间的。 例如extern int a 其中变量a是在别的文件中定义的。

    3. 总之就是把建立空间的声明成为“定义”把不需要建立存储空间的成为“声明”。

    • extern
      • 普通变量、类。结构体
    • exportC++中新增
      • 和exturn类似但是用作模板
      • 使用该关键字可实现模板函数的外部调用
      • 模板实现的时候前面加上export别的文件包含头文件就可用该模板

    extern"C"的用法

    在C语言的头文件中对其外部函数只能指定为extern类型C语言中不支持extern "C"声明在.c文件中包含了extern "C"时会出现编译语法错误。**所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。
    C++中调用C代码

    //xx.h
    extern int add(...)
    
    //xx.c
    int add(){
        
    }
    
    //xx.cpp
    extern "C" {
        #include "xx.h"
    }
    

    C调用C++函数

    //xx.h
    extern "C"{
        int add();
    }
    //xx.cpp
    int add(){    
    }
    //xx.c
    extern int add();
    

    1.19 C++中explicit的作用

    explicit阻止隐式转换

    • 隐式转换

      String s1 = "hello";
      //进行隐式转换等价于
      String s1 = String("hello");
      
    • explicit阻止隐式转换

      class Test1
      {
      public
      	Test1(int n){ num = n }
      private:
      	int num;
      }
      
      class Test2
      {
      public
      	explicit Test2(int n){ num = n }
      private:
      	int num;
      }
      
      int main()
      {
      	Test1 t1 = 1; //正确隐式转换
      	Test2 t2 = 1;//错误禁止隐式转换
      	Test2 t2(1); //正确可与显示调用
      }
      

    1.20 C++的异常处理

    C++中的异常处理机制主要使用try、throw和catch三个关键字

    #include <iostream>
    using namespace std;
    int main()
    {
        double m = 1, n = 0;
        try {
            cout << "before dividing." << endl;
            if (n == 0)
                throw - 1;  //抛出int型异常
            else if (m == 0)
                throw - 1.0;  //拋出 double 型异常
            else
                cout << m / n << endl;
            cout << "after dividing." << endl;
        }
        catch (double d) {
            cout << "catch (double)" << d << endl;
        }
        catch (...) {
            cout << "catch (...)" << endl;
        }
        cout << "finished" << endl;
        return 0;
    }
    //运行结果
    //before dividing.
    //catch (...)
    //finished
    

    代码中对两个数进行除法计算其中除数为0。可以看到以上三个关键字

    • 程序的执行流程是先执行try包裹的语句块如果执行过程中没有异常发生则不会进入任何catch包裹的语句块如果发生异常则使用throw进行异常抛出再由catch进行捕获
    • throw可以抛出各种数据类型的信息代码中使用的是数字也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获不会出现类型转换如果匹配不到就直接报错可以使用catch(…)的方式捕获任何异常不推荐。
    • 当然如果catch了异常当前函数如果不进行处理或者已经处理了想通知上一层的调用者可以在catch里面再throw异常。

    1.21 回调函数

    • 把一段可执行的代码像参数传递那样传给其他代码而这段代码会在某个时刻被调用执行这就叫做回调。

    • 如果代码立即被执行就称为同步回调如果过后再执行则称之为异步回调。

    • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针地址作为参数传递给另一个函数当这个指针被用来调用其所指向的函数时我们就说这是回调函数。

    • 主函数和回调函数是在同一层的而库函数在另外一层。如果库函数对我们不可见我们修改不了库函数的实现也就是说不能通过修改库函数让库函数调用普通函数那样实现那我们就只能通过传入不同的回调函数

    • sort()中自定义的cmp就是回调函数

    1.22 C++中mutable的作用

    mutable的中文意思是“可变的易变的”在C++中mutable也是为了突破const的限制而设置的。被mutable修饰的变量将永远处于可变的状态即使在一个const函数中

    class person
    {
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
    public:
         void add() const//在函数里不可修改this指针指向的值 常量指针
         {
            m_A=10;//错误  不可修改值this已经被修饰为常量指针
            m_B=20;//正确
         }
    }
    
    int main()
    {
    	const person p;//修饰常对象 不可修改类成员的值
    	p.m_A=10;//错误被修饰了指针常量
    	p.m_B=200;//正确特殊变量修饰了mutable
    }
    

    2. 内存分配

    2.1 C++内存分配

    见 1.8

    2.2 内存泄漏

    内存泄露的原因

    内存泄漏是指堆内存的泄漏。使用malloc,、realloc、 new等函数从堆中分配到块内存使用完后程序必须负责相应的调用free或delete释放该内存块如果没有释放内存这块内存就不能被再次使用我们就说这块内存泄漏了

    避免内存泄露的几种方式

    • 计数法使用new或者malloc时让该数+1delete或free时该数-1程序执行完打印这个计数如果不为0则表示存在内存泄露
    • 一定要将基类的析构函数声明为虚函数这样子类的析构函数必须重新实现避免忘记释放内存
    • 对象数组的释放一定要用delete []
    • 有new就有delete有malloc就有free保证它们一定成对出现

    内存泄漏检测工具

    • 从Linux下可以使用Valgrind工具
    • Windows下可以使用CRT库

    2.3 栈默认的大小

    • Windows 下是2MB
    • Linux下是8MBulimit-s 设置

    2.4 sizeof() 和 strlen()的区别

    int a ,b;
    a = strlen("\0");
    b = sizeof("\0");
    // a = 0 , b = 2;
    
    • sizeof()是c关键字计算内存大小字节单位

    • strlen()是函数计算字符串的长度到\0结束

    • sizeof是编译确定的strlen是运行确定的

      int a,b,c;
      char str[20] = "0123456789";
      const char *str2 = "0123456789";
      a = strlen(str);
      b = sizeof(str);
      c = sizeof(&str);
      d = strlen(str2);
      e = sizeof(str2);
      // a = 10 , b = 20 , c = 4(指针大小);
      // d = 10 , e = 4(指针大小)
      

    2.5 struct结构体的数据对齐

    为什么结构体的sizeof返回值一般大于期望

    • struct的sizeof是所有成员数据对其后长度相加
    • union的sizeof是取最大的成员长度所有成员共用一个内存

    struct数据对其的目的

    • 是编译器的一种计算手段在空间和复杂度上的平衡在空间浪费可接收的前提下cpu运算最快处理

    • 32位数据传输是4字节数据字长struct进行4的倍数对其。64位数据传输是8字节8的倍数对其

    • 对齐的目的是要让数据访问更高效一般来说数据类型的对齐要求和它的长度是一致的比如

        char 是 1
        short 是 2
        int 是 4
        double 是 8
      
      • 这不是巧合比如short2对齐保证了short只可能出现在一个读取单元的0, 2, 4, 6格而不会出现在1, 3, 5, 7格
      • 再比如int4对齐保证了一个读取单元可以装载2个int——在0或者4格。
      • 从根本上杜绝了同一个数据横跨读取单元的问题。

    修改默认的数据对齐

    • #pragma pack(n)编译器按照n字节对其
    • #pragma pack( )取消自定义对其
    • __ attribute__((aligned(n))) 让结构体成员对其在n字节自然边界上如果成员大于n按照最大成员长度
    • __ attribute__((packed))取消编译过程的对齐按照实际占用字节对其

    C++11中内存对其关键字

    • alignof计算出类型对齐的方式
    • alignas指定结构体的对齐方式
    struct Info {
      uint8_t a;
      uint16_t b;
      uint8_t c;
    };
    
    std::cout << sizeof(Info) << std::endl;   // 6  2 + 2 + 2
    std::cout << alignof(Info) << std::endl;  // 2
    
    //alignas将内存对齐调整为4个字节。所以sizeof(Info2)的值变为了8。
    struct alignas(4) Info2 {
      uint8_t a;
      uint16_t b;
      uint8_t c;
    };
    
    std::cout << sizeof(Info2) << std::endl;   // 8  4 + 4
    std::cout << alignof(Info2) << std::endl;  // 4
    

    若alignas小于自然对齐的最小单位则被忽略。

    2.6 堆和栈的区别

    • 申请方式不同。

      • 栈由系统自动分配。
      • 堆是自己申请和释放的。
    • 申请大小限制不同。

      • 栈顶和栈底是之前预设好的栈是向栈底扩展大小固定可以通过ulimit -a查看由ulimit -s修改。

      • 堆向高地址扩展是不连续的内存区域大小可以灵活调整。

    • 申请效率不同。

      • 栈由系统分配速度快不会有碎片。

      • 堆由程序员分配速度慢且会有碎片。

    • 栈空间默认是4M, 堆区一般是 1G - 4G

    • 速度不同

      • 毫无疑问是栈快一点。

      • 因为操作系统会在底层对栈提供支持会分配专门的寄存器存放栈的地址栈的入栈出栈操作也十分简单并且有专门的指令执行所以栈的效率比较高也比较快。

      • 而堆的操作是由C/C++函数库提供的在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问第一次访问指针第二次根据指针保存的地址访问内存因此堆比较慢。

    2.7 形参和实参的区别

    • 形参变量只有在被调用时才分配内存单元在调用结束时 即刻释放所分配的内存单元。

    • 实参可以是常量、变量、表达式、函数等 无论实参是何种类型的量在进行函数调用时它们都必须具有确定的值 以便把这些值传送给形参。

    • 实参和形参在数量上类型上顺序上应严格一致 否则会发生“类型不匹配”的错误。

    • 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参而不能把形参的值反向地传送给实参。

    • 当形参和实参不是指针类型时在该函数运行时形参和实参是不同的变量他们在内存中位于不同的位置形参将实参的内容复制一份在该函数运行结束的时候形参被释放而实参内容不会改变。

    3. 指针

    3.1 指针的优点

    指针变量和一般变量区别一般变量是包含的是数据而指针变量包含的是地址

    • 动态分配内存直接操作内存效率高
    • 实现动态数据结构树、链表
    • 高效的“复制”数据

    3.2 引用和指针

    • 指针是一个变量存储的是一个地址引用跟原来的变量实质上是同一个东西是原变量的别名
    • 指针可以有多级引用只有一级
    • 指针可以为空引用不能为NULL且在定义时必须初始化
    • 指针在初始化后可以改变指向而引用在初始化之后不可再改变
    • sizeof指针得到的是本指针的大小4字节sizeof引用得到的是引用所指向变量的大小
    • 当把指针作为参数进行传递时也是将实参的一个拷贝传递给形参两者指向的地址相同但不是同一个变量在函数中改变这个变量的指向不影响实参而引用却可以。
    • 引用本质是一个指针同样会占4字节内存指针是具体变量需要占用存储空间具体情况还要具体分析。
    • 引用在声明时必须初始化为另一变量指针声明和定义可以分开可以先只声明指针变量而不初始化等用到时再指向具体变量。

    3.3 数组和指针

    • 数组在内存中是连续存放的开辟一块连续的内存空间数组所占存储空间sizeof数组名数组大小sizeof(数组名)/sizeof(数组元素数据类型)

    • 用运算符sizeof 可以计算出数组的容量字节数。sizeof( p ),p 为指针得到的是一个指针变量的字节数4而不是p 所指的内存容量。

    • 编译器为了简化对数组的支持实际上是利用指针实现了对数组的支持。具体来说就是将表达式中的数组元素引用转换为指针加偏移量的引用。

    • 在向函数传递参数的时候如果实参是一个数组那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组能够提高效率

    • 在使用下标的时候两者的用法相同都是原地址加上下标值不过数组的原地址就是数组首元素的地址是固定的指针的原地址就不是固定的。

    数组名和指针

    • 二者均可通过增减偏移量来访问数组中的元素。

    • 数组名不是真正意义上的指针可以理解为常指针所以数组名没有自增、自减等操作。

    • 当数组名当做形参传递给调用函数后就失去了原有特性退化成一般指针多了自增、自减操作但sizeof运算符不能再得到原数组的大小了。

    3.4 指针的加法

    指针加上n为加上n个指针类型的长度

    unsigned char*p1 = 0x801000;
    unsigned int *p2 = 0x810000;
    p1+=5;//p1 = 0x801000 + 5*1 = 0x801005;
    p2+=5;//p2 = 0x810000 + 5*4 = 0x810000;
    

    3.5 空指针、野指针和悬空指针

    • 空指针
      空指针不会指向任何地方它不是任何对象或函数的地址

      int *p = NULL;
      int *p2 = nullptr;
      
    • 野指针
      指的是没有被初始化过的指针

      int main(void) { 
          
          int* p;     // 未初始化
          std::cout<< *p << std::endl; // 未初始化就被使用
          
          return 0;
      }
      
    • 悬空指针
      最初指向的内存已经被释放了的一种指针

      int main(void) { 
        int * p = nullptr;
        int* p2 = new int;
        p = p2;
        delete p2;
      }
      

      此时 p和p2就是悬空指针指向的内存已经被释放。继续使用这两个指针行为不可预料

      野指针和悬空指针的产生和解决

    • 野指针指针变量未及时初始化 => 定义指针变量及时初始化要么置空

    • 悬空指针指针free或delete之后没有及时置空 => 释放操作后立即置空
      使用智能指针避免悬空指针产生

    3.6 指针函数和函数指针的区别

    指针函数

    • 返回值为指针类型的函数

      #include<stdio.h>
      int* fun(int* x)    //传入指针  
      {
      	int* tmp = x;	  //指针tmp指向x
          return tmp;       //返回tmp指向的地址
      }
      int main()
      {
          int b = 2;      
          int* p = &b;   //p指向b的地址
          printf("%d",*fun(p));//输出p指向的地址的值
          return 0;
      }
      

    函数指针

    • 函数指针是 指向函数的指针 。主体是指针指向的是一个函数的地址

    • 两种方法赋值指针名 = 函数名 指针名 = &函数名

      #include<stdio.h>
      int add(int x,int y)
      {
          return x + y;
      }
      
      int main()
      {
      	int (*fun) (int,int);//声明函数指针
          fun = &add;		//fun函数指针指向add函数
          //fun = add;   //同上等价fun = &add;
          printf("%d ",fun(3,5));		
          printf("%d",(*fun)(4,2));
          return 0;
      }
      

    3.7 传递函数参数的时候什么时候使用指针什么时候使用引用

    • 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存用完要记得释放指针不然会内存泄漏。而返回局部变量的引用是没有意义的

    • 对栈空间大小比较敏感比如递归的时候使用引用。使用引用传递不需要创建临时变量开销要更小

    • 类对象作为参数传递的时候使用引用这是C++类对象传递的标准方式

    3.8 区别指针类型

    int *p[10]
    int (*p)[10]
    int *p(int)
    int (*p)(int)
    
    • int *p[10]表示指针数组强调数组概念是一个数组变量数组大小为10数组内每个元素都是指向int类型的指针变量。

    • int (*p)[10]表示数组指针强调是指针只有一个变量是指针类型不过指向的是一个int类型的数组这个数组大小是10。

    • int *p(int)是函数声明函数名是p参数是int类型的返回值是int *类型的。

    • int (*p)(int)是函数指针强调是指针该指针指向的函数具有int类型参数并且返回值是int类型的。

    3.9 int a[10]; int (*p)[10] = &a中a和&a有什么区别

    • a是数组名是数组首元素地址+1表示地址值加上一个int类型的大小如果a的值是0x00000001加1操作后变为0x00000005。*(a + 1) = a[1]。
    • &a是数组的指针其类型为int (*)[10]就是前面提到的数组指针其加1时系统会认为是数组首地址加上整个数组的偏移10个int型变量值为数组a尾元素后一个元素的地址。
    • 若(int *)p 此时输出 *p时其值为a[0]的值因为被转为int *类型解引用时按照int类型大小来读取。

    3.10 值传递、指针传递、引用传递的区别和效率

    • 值传递有一个形参向函数所属的栈拷贝数据的过程如果值传递的对象是类对象 或是大的结构体对象将耗费一定的时间和空间。传值

    • 指针传递同样有一个形参向函数所属的栈拷贝数据的过程但拷贝的数据是一个固定为4字节的地址。传值传递的是地址值

    • 引用传递同样有上述的数据拷贝过程但其是针对地址的相当于为该数据所在的地址起了一个别名。传地址

    效率上讲指针传递和引用传递比值传递效率高。一般主张使用引用传递代码逻辑上更加紧凑、清晰。

    4. 预处理

    为编译做准备工作处理#开头的指令
    在这里插入图片描述

    4.1 ifndef/define/endif的作用

    防止头文件被重复包含和编译。头文件重复包含会增大程序大小重复编译增加编译时间

    4.2 #include< > 和 #include“ ”的区别

    • <>和" "表示编译器在搜索头文件时的顺序不同
    • <>表示从系统目录下开始搜索然后再搜索PATH环境变量所列出的目录不搜索当前目录
    • ""是表示从当前目录开始搜索然后是系统目录和PATH环境变量所列出的目录。
      所以系统头文件一般用<>用户自己定义的则可以使用""加快搜索速度

    4.3 #define 的缺点

    #define只能进行字符替换

    • 无法类型检查

    • 由于优先级的不同会产生潜在问题

      #define MAX_NUM 100+1
      int a = MAX_NUM * 10;//a=110
      //等价于
      int a = 100 + 1 * 10;
      
      //正确定义为
      #define MAX_NUM 100+1
      int a = MAX_NUM * 10;//a=1010
      
    • 无法单步调试

    • 导致代码膨胀

    4.4 写一个标准宏MIN

    #define MIN(A,B) ( (A)<=(B)?(A):(B) )
    

    每个括号都是必须的如果没有结果无法预测

    4.5 #define和typdef的区别

    • define主要用于定义常量及书写复杂的内容typedef主要用于定义类型别名。

    • define替换发生在编译阶段之前属于文本插入替换typedef是编译的一部分。

    • define不检查类型typedef会检查数据类型。

    • define不是语句不在在最后加分号typedef是语句要加分号标识结束。

    • 对指针的操作不同

      #define INTPTR1 int*
      typedef int* INTPTR2;
      INTPTR1 p1, p2;//声明一个指针变量p1和一个整型变量p2
      INTPTR2 p3, p4;//声明两个指针变量p3、p4
      
      #define INTPTR1 int*
      typedef int* INTPTR2;
      int a = 1;
      int b = 2;
      int c = 3;
      const INTPTR1 p1 = &a;//const INTPTR1 p1是一个常量指针
      const INTPTR2 p2 = &b;//const INTPTR2 p2是一个指针常量
      INTPTR2 const p3 = &c;//INTPTR2 const p3是一个指针常量
      

    4.6 宏定义和内联函数的区别

    • 宏定义是在预处理阶段进行代码替换内联函数是编译阶段插入代码
    • 宏定义没有类型检查内敛函数有类型检查

    内联函数和普通函数的区别

    • 编译器将内联函数的位置进行函数展开避免函数调用的开销提高效率
    • 普通函数被调用跳跃到函数入口地址执行结束后跳转回调用地方
    • 内敛函数不需要寻址执行N次内联函数代码就复制N次
    • 函数体过大编译器放弃内联变的和普通函数一样
    • 内联函数不能递归编译器无法预知深度变成普通函数

    4.7 #define和const区别

    • #define只能单纯文本替换不分配周期寸在代码段
    • const常量在程序数据段分配内存
    • #define没有数据类型const有数据类型
    • #define没法调试const可以调试

    5.结构体与类

    5.1 struct和union的区别

    • 联合体所有成员共用一块内存结构体成员占用空间累加
    • 对联合体的不用成员赋值对其他成员重写结构体成员互相不影响
    union
    {
    	int i;
    	char x[2];
    }
    int main()
    {
    	a.x[0] = 10;
    	a.x[1] = 1;
    	printf("%d",a.i);//输出为266
    }
    

    其中 a.x[0]=10=00001010 a.x[1] = 1 = 00000001。
    输出 i 的时候将a.x[0] a.x[1] 看作一个整数为00000001 00001010为256+8+2 = 266

    在这里插入图片描述

    5.2 C++中struct和class的区别

    相同点

    • 两者都拥有成员函数、公有和私有部分

    • 任何可以使用class完成的工作同样可以使用struct完成
      不同点

    • 两者中如果不对成员不指定公私有struct默认是公有的class则默认是私有的

    • class默认是private继承 而struct默认是public继承

    5.3 C++和C的struct区别

    • C语言中struct是用户自定义数据类型UDTC++中struct是抽象数据类型ADT支持成员函数的定义C++中的struct能继承能实现多态

    • C中struct是没有权限的设置的且struct中只能是一些变量的集合体可以封装数据却不可以隐藏数据而且成员不可以是函数

    • C++中struct增加了访问权限且可以和类一样有成员函数成员默认访问说明符为public为了与C兼容

    • struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后在C中必须在结构标记前加上struct才能做结构类型名除typedef struct class{};;C++中结构体标记结构体名可以直接作为结构体类型名使用此外结构体struct在C++中被当作类的一种特例

    6. 位操作

    6.1 最有效的计算2乘8的方法

    int a = 2 ;
    a = a<<3;//a乘上2的三次方
    

    计算乘7倍

    int a = 2 ;
    a = a<<3-a;
    

    6.2 位操作求两个数的平均值

    int a = 2 .b =3;
    int c ;
    c = (a&b) + ((a^b)>>1);
    
    • 对于表达式(x&y)+(xy)>>1), x&y表示的是取出x与y二进制位数中都为1的所有位, xy表示的是x与y中有一个为1’的所有位,右移1位相当于执行除以2运算。
    • 整个表达式实际上可以分为两部分,第一部分是都为1的部分,求平均数后这部分的值保持不变;而第二部分是x为1、y为0的部分,以及y为1、x为0的部分,两部分加起来再除以2,然后跟前面的相加就可以表示两者的平均数了

    6.3 什么是大端和小端如何判断

    • 小端存储字数据的低字节存储在低地址中数据存储从低字节到高字节
    • 大端存储字数据的高字节存储在低地址中数据存储从高字节到低字节
      例如32bit的数字0x12345678

    在这里插入图片描述

    代码判断

    • 方式一使用强制类型转换-这种法子不错
    #include <iostream>
    using namespace std;
    int main()
    {
        int a = 0x1234;
        //由于int和char的长度不同借助int型转换成char型只会留下低地址的部分
        char c = (char)(a);
        if (c == 0x12)
            cout << "big endian" << endl;
        else if(c == 0x34)
            cout << "little endian" << endl;
    }
    
    • 方式二巧用union联合体
    #include <iostream>
    using namespace std;
    //union联合体的重叠式存储endian联合体占用内存的空间为每个成员字节长度的最大值
    union endian
    {
        int a;
        char ch;
    };
    int main()
    {
        endian value;
        value.a = 0x1234;
        //a和ch共用4字节的内存空间
        if (value.ch == 0x12)
            cout << "big endian"<<endl;
        else if (value.ch == 0x34)
            cout << "little endian"<<endl;
    }
    
    

    7. 编译

    7.1 main函数执行前和执行后的代码

    main函数执行前初始化系统相关资源

    • 设置栈指针
    • 初始化静态static变量和global全局变量即.data段的内容
    • 将未初始化部分的全局变量赋初值数值型shortintlong等为0bool为FALSE指针为NULL等等即.bss段的内容
    • 全局对象初始化在main之前调用全局对象的构造函数这是可能会执行前的一些代码
    • 将main函数的参数argcargv等传递给main函数然后才真正运行main函数
    • __ attribute __((constructor))

    main函数执行后

    • 全局对象的析构函数会在main函数之后执行
    • 可以用 atexit 注册一个函数它会在main 之后执行;
    • __ attribute__((destructor))

    8.面向对象

    8.1 final和override关键字

    override

    override指定了子类的这个虚函数是重写的父类的如果你名字不小心打错了的话编译器是不会编译通过的。

    class A
    {
        virtual void foo();
    };
    class B : public A
    {
        virtual void f00(); //OK这个函数是B新增的不是继承的
        virtual void f0o() override; //Error, 加了override之后这个函数一定是继承自A的A找不到就报错
        //virtual void foo() override; //ok是继承父类的虚函数
    };
    

    final

    不希望某个类被继承或不希望某个虚函数被重写可以在类名虚函数后添加final关键字添加final关键字后被继承或重写编译器会报错。

    class Base
    {
        virtual void foo();
    };
     
    class A : public Base
    {
        void foo() final; // foo 被override并且是最后一个override在其子类中不可以重写
    };
    
    class B final : A // 指明B是不可以被继承的
    {
        void foo() override; // Error: 在A中已经被final了
    };
     
    class C : B // Error: B is final
    {
    };
    

    8.2 拷贝初始化和直接初始化

    当用于类类型对象时初始化的拷贝形式和直接形式有所不同

    • 直接初始化直接调用与实参匹配的构造函数
    • 拷贝初始化首先使用指定构造函数创建一个临时对象然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
    //语句1 直接初始化
    string str1("I am a string");
    //语句2 直接初始化str1是已经存在的对象直接调用拷贝构造函数对str2进行初始化
    string str2(str1);
    //语句3 拷贝初始化先为字符串”I am a string“创建临时对象再把临时对象作为参数使用拷贝构造函数构造str3
    string str3 = "I am a string";
    //语句4 拷贝初始化这里相当于隐式调用拷贝构造函数而不是调用赋值运算符函数
    string str4 = str1;
    
    • 为了提高效率允许编译器跳过创建临时对象这一步直接调用构造函数构造要创建的对象这样就完全等价于直接初始化了语句1和语句3等价但是需要辨别两种情况。
      • 当拷贝构造函数为private时语句3和语句4在编译时会报错
      • 使用explicit修饰构造函数时如果构造函数存在隐式转换编译时会报错

    8.3 C和C++的类型安全

    类型安全很大程度上可以等价于内存安全类型安全的代码不会试图访问自己没被授权的内存区域。有的时候也用“类型安全”形容某个程序判别的标准在于该程序是否隐含类型错误。

    • 想保证程序的类型安全性
      • 应尽量避免使用空类型指针void*
      • 尽量不对两种类型指针做强制转换。

    8.4 C++中的重载、重写覆盖和隐藏的区别

    重载overload

    重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同参数类型和数目有所不同不能出现参数个数和类型均相同仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。

    class A{
        ...
        virtual int fun();
        void fun(int);
        void fun(double, double);
        static int fun(char);
        ...
    }
    

    重写覆盖override

    重写指的是在派生类中覆盖基类中的同名函数重写就是重写函数体要求基类函数必须是虚函数且

    • 与基类的虚函数有相同的参数个数
    • 与基类的虚函数有相同的参数类型
    • 与基类的虚函数有相同的返回值类型
    //父类
    class A{
    public:
        virtual int fun(int a){}
    }
    //子类
    class B : public A{
    public:
        //重写,一般加override可以确保是重写父类的函数
        virtual int fun(int a) override{}
    }
    

    重载与重写的区别

    • 重写是父类和子类之间的垂直关系重载是不同函数之间的水平关系
    • 重写要求参数列表相同重载则要求参数列表不同返回值不要求

    隐藏hide

    隐藏指的是某些情况下派生类中的函数屏蔽了基类中的同名函数包括以下情况

    • 两个函数参数相同但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。

      //父类
      class A{
      public:
          void fun(int a){
      		cout << "A中的fun函数" << endl;
      	}
      };
      //子类
      class B : public A{
      public:
          //隐藏父类的fun函数
          void fun(int a){
      		cout << "B中的fun函数" << endl;
      	}
      };
      int main(){
          B b;
          b.fun(2); //调用的是B中的fun函数
          b.A::fun(2); //调用A中fun函数
          return 0;
      }
      
    • 两个函数参数不同无论基类函数是不是虚函数都会被隐藏。和重载的区别在于两个函数不在同一个类中

      //父类
      class A{
      public:
          virtual void fun(int a){
      		cout << "A中的fun函数" << endl;
      	}
      };
      //子类
      class B : public A{
      public:
          //隐藏父类的fun函数
         virtual void fun(char* a){
      	   cout << "A中的fun函数" << endl;
         }
      };
      int main(){
          B b;
          b.fun(2); //报错调用的是B中的fun函数参数类型不对
          b.A::fun(2); //调用A中fun函数
          return 0;
      }
      
    • 基类指针指向派生类对象时基类指针可以直接调用到派生类的覆盖重写函数也可以通过 :: 调用到基类被覆盖
      的虚函数

    • 而基类指针只能调用基类的被隐藏函数无法识别派生类中的隐藏函数。

      // 父类
      class A {
      public:
          virtual void fun(int a) { // 虚函数
              cout << "This is A fun " << a << endl;
          }  
          void add(int a, int b) {
              cout << "This is A add " << a + b << endl;
          }
      };
      
      // 子类
      class B: public A {
      public:
          void fun(int a) override {  // 覆盖重写
              cout << "this is B fun " << a << endl;
          }
          void add(int a) {   // 隐藏
              cout << "This is B add " << a + a << endl;
          }
      };
      
      int main() {
          A *p = new B();
          p->fun(1);      // 调用子类 fun 覆盖函数
          p->A::fun(1);   // 调用父类 fun
          p->add(1, 2);
          // p->add(1);      // 错误识别的是 A 类中的 add 函数参数不匹配
          // p->B::add(1);   // 错误无法识别子类 add 函数
          return 0;
      }
      

    8.5 浅拷贝和深拷贝的区别

    浅拷贝

    浅拷贝只是拷贝一个指针并没有新开辟一个地址拷贝的指针和原来的指针指向同一块地址如果原来的指针所指向的资源释放了那么再释放浅拷贝的指针的资源就会出现错误。

    深拷贝

    深拷贝不仅拷贝值还开辟出一块新的空间用来存放新的值即使原先的对象被析构掉释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候如果有指针变量的话是需要自己实现深拷贝的。

    8.6 publicprotected和private访问和继承权限

    访问权限

    在这里插入图片描述

    public、protected、private 的访问权限范围关系public > protected > private

    继承权限

    1. public继承
      公有继承的特点是基类的公有和保护变派生类的公有和保护基类私有不可访问

    2. protected继承
      保护继承的特点是基类的公有和保护变派生类的保护基类私有派生类不可访问

    3. private继承
      私有继承的特点是基类的公有和保护变派生类的私有基类私有派生类不可访问

    在这里插入图片描述

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
    标签: c++