admin管理员组

文章数量:1630589

文章目录

  • 可变参数-printf的实现原理
  • 编译语言和解释语言
  • C++空指针调用成员函数
  • std::move
    • 移动语义
    • std::move的实现
    • 完美转换
  • malloc函数底层实现
  • 被free回收的内存是立即返还给操作系统吗?
  • 定义和声明的区别
  • 调试程序的方法
    • 遇到coredump要怎么调试
  • 引用作为函数参数以及返回值的好处
  • 成员初始化列表的概念,为什么用成员初始化列表会快一些
  • this指针,类对象调用普通的成员函数
  • 定义常量/处理返回值/拷贝赋值函数的形参只能引用
  • 编译底层
  • 内存区域的划分和分配
    • 内存分配方式
    • LINUX进程区分段及存储数据
  • 被free回收的内存是立即返还给操作系统吗?为什么
  • 动态链接和静态链接
    • GCC编译流程
    • 动态库静态库区别及GCC加载库
  • C++和C的区别
  • C++三大特性:
  • 模板的全特化与偏特化
    • 类模板:
    • 函数模板
  • C语言实现多态
  • C++11新特性
  • C++11新特性之十:enable_shared_from_this
  • inline和define的特点和区别
  • define和const的联系与区别
  • class和struct的区别
  • new/delete和malloc/free区别
  • const关键字
  • static关键字
  • volatile关键字
  • explicit关键字
  • extern
  • restrict
  • override与final
  • typedef介绍
    • 定义函数指针类型
    • 定义数组指针类型
    • 定义数组类型
    • 给已定义的变量类型起个别名
  • size_t和int
  • 指针常量与常量指针
  • 指针和引用的区别?
  • 栈和堆的区别
  • 4种强制转换
  • 野指针
  • 智能指针
    • 智能指针的内存泄漏如何解决
    • 内存泄漏
    • 谨慎使用裸指针
    • 不要使用静态分配对象的指针初始化智能指针
  • allocator类
  • C/C++内存泄露及其检测工具
  • C++所有的构造函数
  • 左值与右值引用
  • 结构体内存对齐方式
  • 什么情况下会调用拷贝构造函数(三种情况)
  • 深拷贝和浅拷贝
  • 对象复用的了解,零拷贝的了解
  • 友元全局函数、友元类、友元成员函数
  • 函数指针与指针函数
  • 什么是操作系统的原子操作
  • i++和++i实现
  • 多继承和菱形继承
      • 多继承
      • 菱形继承
        • 菱形继承会产生问题
  • 重载,重写(覆盖)和隐藏的区别
      • 运算符重载总结
  • 构造函数为什么不能是虚函数,而析构函数是虚函数
  • 为什么析构函数必须是虚函数
  • 构造函数或者析构函数中调用虚函数会怎样
  • 怎么理解多态和虚函数
  • 抽象类和纯虚函数
  • 纯虚析构
  • 类模板和函数模板
  • 只在堆上/栈上
    • 类只能在堆上创建对象
    • 类只能在栈上创建对象
  • 静态链表和动态链表区别:
  • 迭代器与指针的差别
  • 析构顺序
  • 头文件避免重复引用
  • 为什么C++没有实现垃圾回收?

可变参数-printf的实现原理

在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来

在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),

  • 而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf(“%d,%d”,a,b);(其中a、b都是int型的)的汇编代码.
.section
.data
string out = "%d,%d"
push b  //最后的先压入栈中
push a //最先的后压入栈中
push $out//参数控制的那个字符串常量是最后被压入的
call printf

参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。

通常情况下函数可变参数表的长度是已知的,通过num参数传入,这种函数比较容易实现。

而printf函数的实现非常复杂因为

  • 1)可变参数的个数不能轻易的得到

  • 2)而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f、%d、%s等确定)

在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( vap, int ),而获知为%l和%lf后则进行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串识别完成后,可变参数也就处理完了。

printf的简单实现:
#include
#include
 
void myitoa(int n, char str[], int radix)
{
int i , j , remain;
char tmp;
i = 0;
do
{
remain = n % radix;
if(remain > 9)
str[i] = remain  - 10 + 'A';
else
str[i] = remain + '0';
i++;
}while(n /= radix);
str[i] = '\0';
 
for(i-- , j = 0 ; j <= i ; j++ , i--)
{
tmp = str[j];
str[j] = str[i];
str[i] = tmp;
}
 
}
 
void myprintf(const char *format, ...)
{
char c, ch, str[30];
va_list ap;
 
va_start(ap, format);
while((c = *format))
{
switch(c)
{
  case '%':
  ch = *++format;
  switch(ch)
  {
    case 'd':
    {
    int n = va_arg(ap, int);
    myitoa(n, str, 10);
    fputs(str, stdout);
    break;
    }
    case 'x':
    {
      int n = va_arg(ap, int);
      myitoa(n, str, 16);
      fputs(str, stdout);
      break;
    }
    case 'f':
    {
      double f = va_arg(ap, double);
      int n;
      n = f;
      myitoa(n, str, 10);
      fputs(str, stdout);
      putchar('.');
      n = (f - n) * 1000000;
      myitoa(n, str, 10);
      fputs(str, stdout);
      break;
    }
    case 'c':
    {
      putchar(va_arg(ap, int));
      break;
    }
    case 's':
    {
      char *p = va_arg(ap, char *);
      fputs(p, stdout);
      break;
    }
    case '%':
    {
      putchar('%');
      break;
    }
    default:
    {
      fputs("format invalid!", stdout);
      break;
    }
  }
  break;
  default:
    putchar(c);
    break;
  }
  format++;
}
va_end(ap);
}
 
int main(void)
{
myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B', "hello");
return 0;
}

编译语言和解释语言

编写的源代码是人类语言,我们自己能够轻松理解;但是对于计算机硬件(CPU),源代码就是天书,根本无法执行,计算机只能识别某些特定的二进制指令,在程序真正运行之前必须将源代码转换成二进制指令。

所谓的二进制指令,也就是机器码,是 CPU 能够识别的硬件层面的“代码”,简陋的硬件(比如古老的单片机)只能使用几十个指令,强大的硬件(PC 和智能手机)能使用成百上千个指令。

  • 有的编程语言要求必须提前将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序(Windows 下的 .exe),比如C语言、C++、Golang、Pascal(Delphi)、汇编等,这种编程语言称为编译型语言,使用的转换工具称为编译器。
  • 有的编程语言可以一边执行一边转换,需要哪些源代码就转换哪些源代码,不会生成可执行程序,比如 Python、JavaScript、PHP、Shell、MATLAB 等,这种编程语言称为解释型语言,使用的转换工具称为解释器。

C++空指针调用成员函数

  • 当被调用的成员函数中,未使用this指针去调用当前实例的成员变量,程序正常运行不报错
  • 当被调用的成员函数中,使用了this指针去读写当前实例的成员变量时,首先调用是成员函数是被成功调用的,代码的执行已经进入了成员函数的领空,但当代码执行到读写当前实例的成员变量里,就会报内存访问异常了。

# 不可以同时用const和static修饰成员函数。

C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们

std::move

左值和右值

左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。

一般来说,变量可以取地址,所以是左值,但是常量和临时对象等不可以取地址,所以是右值。

右值一个重要的性质:只能绑定到一个将要被销毁的对象上,因此我们可以自由的将一个右值引用的资源“移动”到另一个对象中。

  • 虽然不能将一个右值引用直接绑定到一个左值上,但是我们可以显示的将一个左值转换为对应的右值引用类型。名为move的标准库函数来获得绑定到左值上引用

    int &&rr3 = std::move(rr1); //ok
    
  • 比如说常用的序列式容器vector数组,是一种典型的不必要拷贝的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的。更好的方式是移动元素

  • 使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能被共享的资源(如指针或IO缓冲),因此这些类的对象不能拷贝但可以移动。如果对象比较大,进行拷贝的代价比较大。

右值引用的作用与移动语义

左值的声明符号为&,右值的声明符号为&&。

在各种场合(初始化,push_back,函数返回等)调用拷贝构造函数将一个临时对象初始化给另一个对象,而这时如果是深拷贝则代价会比较大。

深拷贝对程序的影响比较大,把临时对象的内容直接移动(move)给被赋值的左值对象,效率改善将是显著的。这就产生了移动语义,右值引用是用来支持转移语义的

移动语义

移动语义可以将资源(堆,系统对象等) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能

移动语义意味着两点:

  • 原对象不再被使用,如果对其使用会造成不可预知的后果。
  • 所有权转移,资源的所有权被转移给新的对象。

移动语义通过移动构造函数移动赋值操作符实现,其与拷贝构造函数类似,区别如下:

  • 参数的符号必须为右值引用符号,即为&&。
  • 参数不可以是常量,因为函数内需要修改参数的值
  • 参数的成员转移后需要修改(如改为nullptr),避免临时对象的析构函数将资源释放掉。

std::move的实现

用std::move可以获得一个绑定到左值上的右值引用。由于move本质上可以接受任何类型的实参,因此他是一个模板。

标准库是这样定义std::move的

    //remove_reference是为了萃取类型的类型转化模板
    template <typename T>
    tylename remove_reference<T>::type && move(T&& t) {

        return std::static_cast<typename remove_reference<T>::type&&>(t);

    }

引用折叠

如果间接的创建一个引用的引用,则这些引用就会“折叠”。如:

  • X& &、X& &&、X&& &都折叠成X&
  • X&& &&折叠为X&&

通过引用折叠,此参数可以与任何类型的实参匹配,既可以传递给move一个左值引用也可以传右值引用,如:

string s1("hi"), s2;
s2 = std::move(string("bye1!"));//正确,从一个右值移动数据
s2 = std::move(s1);//正确,但赋值之后,s1的值是不确定的。 

1)针对std::move(string(“bye1!”)); 传入的已经是右值引用

  • 函数模板推断出T的类型为string
  • 因此,remove_reference用string进行实例化
  • remove_reference的 type成员是string
  • move的返回类型是string&&move的函数参数t的类型是string&&

因此等价于:

string&& move(string&& t);

函数体返回static_cast<string&&>(t),由于t的类型已经是右值引用,因此无需进行任何转化。

2)针对std::move(s1); 传入的就是一个左值

引入一条规则:当将一个左值传递给一个参数是右值引用的函数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用。

  • 函数模板推断出T的类型是string&(因为string& &&才能折叠为string&)
  • 因此,remove_reference用string&进行实例化
  • remove_reference的 type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型是string& &&,会折叠为string&

因此等价于:

string&& move(string& t);

函数体返回static_cast<string&&>(t),这里t的类型是string&,通过static_cast将其转化为string&&

完美转换

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

void process(int& i){
    cout << "process(int&):" << i << endl;
}
void process(int&& i){
    cout << "process(int&&):" << i << endl;
}
 
void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(i);
}
 
int main()
{
    int a = 0;
    process(a); //a被视为左值 process(int&):0
    process(1); //1被视为右值 process(int&&):1
    process(move(a)); //强制将a由左值改为右值 process(int&&):0
    myforward(2);  //右值经过forward函数转交给process函数,却称为了一个左值,
    //原因是该右值有了名字  所以是 process(int&):2
    myforward(move(a));  // 同上,在转发的时候右值变成了左值  process(int&):0
    // forward(a) // 错误用法,右值引用不接受左值
}

c++中提供了一个std::forward()模板函数解决这个问题

void myforward(int&& i)
{
    cout << "myforward(int&&):" << i << endl;
    process(std::forward<int>(i));
}
 
myforward(2); // process(int&&):2

上面修改过后还是不完美转发,myforward()函数能够将右值转发过去,但是并不能够转发左值,解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。例子如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
 
void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}
 
// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}
 
template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}
 
int main()
{
    int a = 0;
    int b = 0;
    const int c = 0;
    const int d = 0;
 
    notPerfectForward(a); // lvalue ref
    notPerfectForward(move(b)); // lvalue ref
    notPerfectForward(c); // const lvalue ref
    notPerfectForward(move(d)); // const lvalue ref
 
    cout << endl;
    perfectForward(a); // lvalue ref
    perfectForward(move(b)); // rvalue ref
    perfectForward(c); // const lvalue ref
    perfectForward(move(d)); // const rvalue ref
}

malloc函数底层实现

malloc和new调用mmap时仅仅申请虚拟地址空间,并不实际获得物理内存空间,因此实际的物理内存并没有消耗,只有应用进程访问虚拟地址空间时,才会触发缺页异常,才会导致内核实际去分配物理内存,并更新页表映射关系。使用下述代码进行测试(使用top和free工具)

memset操作malloc的指针,是操作物理内存还是虚拟内存?

https://blog.csdn/yusiguyuan/article/details/39496057

从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap

  • brk是将进程数据段(.data)的最高地址指针向高处移动,这一步可以扩大进程在运行时的堆大小
  • mmap是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,这一步可以获得一块可以操作的堆内存。

通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。

进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。

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

https://blog.csdn/YMY_mine/article/details/81180168

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

定义和声明的区别

  • 声明是告诉编译器变量的类型和名字,不会为变量分配空间
  • 定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次

调试程序的方法

通过设置断点进行调试

打印log进行调试

打印中间结果进行调试

遇到coredump要怎么调试

coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

  • 使用gdb命令对core文件进行调试

以下例子在Linux上编写一段代码并导致segment fault 并产生core文件

mkdir coredumpTest``vim coredumpTest.cpp

在编辑器内键入

#include<stdio.h>``int` `main(){``  ``int` `i;``  ``scanf(``"%d"``,i);``//正确的应该是&i,这里使用i会导致segment fault``  ``printf(``"%d\n"``,i);``  ``return` `0``;``}

编译

g++ coredumpTest.cpp -g -o coredumpTest

运行

./coredumpTest

使用gdb调试coredump

gdb [可执行文件名] [core文件名]

引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(因为没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性

成员初始化列表的概念,为什么用成员初始化列表会快一些

成员初始化列表就是在类或者结构体的构造函数中,在参数列表后以冒号开头,逗号进行分隔的一系列初始化字段。如下:

class A{
int id;
string name;
FaceImage face;
A(int& inputID,string& inputName,FaceImage& inputFace):id(inputID),name(inputName),face(inputFace){} // 成员初始化列表
};

因为使用成员初始化列表进行初始化的话,会直接使用传入参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程,否则会调用一次传入参数的默认构造函数。所以使用成员初始化列表效率会高一些。

另外,有三种情况是必须使用成员初始化列表进行初始化的:

  • 常量成员的初始化,因为常量成员只能初始化不能赋值
  • 引用类型
  • 没有默认构造函数的对象必须使用成员初始化列表的方式进行初始化

C++ 初始化列表

this指针,类对象调用普通的成员函数

this指针:

  • 类每个兑现,都有一个this指针,不占用类对象空间
  • 作用区分不同的对象,同一个成员函数把类的各个对象的数据区分开。可以用this访问不同对象的成员变量

在编译期中,成员函数其实被编译成了与对象无关的普通函数,但是成员函数需要知道它对应的对象是谁,因为成员函数中一般涉及到访问其对象的数据成员。这个时候this指针就会作为隐藏的第一个参数传入成员函数。this指针的地址就是对象的首地址,知道了首地址之后,成员函数便知道了其对象所在位置,就可以很方便的访问其数据成员了。

定义常量/处理返回值/拷贝赋值函数的形参只能引用

  • C++里是怎么定义常量的?常量存放在内存的哪个位置?

    对于局部常量,存放在栈区;对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;字面值常量,比如字符串,放在常量区。

  • 说说C++如何处理返回值?

    生成一个临时变量,把它的引用作为函数参数传入函数内。

  • C++中拷贝赋值函数的形参能否进行值传递?

    只能引用

    不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。

编译底层

内存区域的划分和分配

内存分配方式

内存分配方式有三种:

  • 静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
  • 上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

自由存储区和堆的区别是:堆是操作系统维护的一块内存,是一个物理概念,而自由存储是C++中通过new与delete动态分配和释放的对象的存储区,是一个逻辑概念

C/C++程序在虚拟内存中的排布大概如下所示

1、栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。

2、堆区(heap) — 在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上

3、**全局区(静态区)(static)—**编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。对于C语言初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。而C++则没有这个区别 - 程序结束后由系统释放

4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)。 存在栈中的数据可以共享。栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

堆是一个运行时数据区,类的(对象从中分配空间。由于要在运行时动态分配内存,存取速度较慢。

栈上的内存是系统自动分配的,压栈和出栈都有相应的指令操作,效率较高,而且分配的内存是连续的,不会产生内存碎片。而堆上的内存需要人为创建,当申请或释放一块内存时,系统需要根据一定的算法在堆空间上寻找合适的内存堆空间,同时修改维护堆空闲空间的链表,然后返回地址给程序,效率比较低,而且容易产生碎片。特别是连续创建和删除占用内存较小的数据或对象时,很容易产生内存碎片。

LINUX进程区分段及存储数据

Linux的每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。
逻辑地址分段如下,自下而上:

  • 代码段。分为只读存储区和代码区,存放字符串是常量和程序机器代码和指令
  • 数据段。存储已初始化的全局变量和静态变量。
  • bss段。存储未初始化的全局变量和静态变量,及初始化为0的全局变量和静态变量
  • 堆。 当进程未调用malloc时是没有堆段的,malloc/free开辟的内存空间,向上生长
  • 映射区。存储动态链接库以及调用mmap函数进行的文件映射
  • 栈。存储函数的返回地址、参数、局部变量、返回值,向下生长。

被free回收的内存是立即返还给操作系统吗?为什么

https://blog.csdn/YMY_mine/article/details/81180168

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

动态链接和静态链接

GCC编译流程

  • 预处理阶段:

    编译器将C源代码中的包含的头文件如stdio.h添加进来,将宏定义展开、根据条件编译命令选择要使用的代码,生成.i文件

    gcc -E hello.c -o hello.i
    
  • 编译阶段:编译就是把C/C++代码(比如上面的".i"文件)“编译”成汇编代码,生成.s文件

    gcc –S hello.i –o hello.s
    
  • 汇编阶段:把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件(obj)

    gcc –c hello.s –o hello.o
    
  • 链接阶段:将编译输出obj文件链接成最终可执行文件

    gcc hello.o –o hello
    

静态链接:静态链接使用静态库进行链接,生成的程序包含程序运行所需要的全部库,可以直接运行,不过静态链接生成的程序体积较大。

gcc -o hello_shared hello.o

动态链接:动态链接使用动态链接库进行链接,生成的程序在执行的时候需要加载所需的动态库才能运行。 动态链接生成的程序体积较小,但是必须依赖所需的动态库,否则无法执行。

gcc -static -o hello_static hello.o

动态库静态库区别及GCC加载库

静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。

动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。

静态库

  • 编译时期链接
  • 浪费空间和资源,如果多个程序链接了同一个库,则每一个生成的可执行文件就都会有一个库的副本,必然会浪费系统空间。
  • 若静态库需修改,需重新编译所有链接该库的程序

动态库

  • 运行时链接
  • 运行时被链接,故程序的运行速度稍慢
  • 动态库是在程序运行时被链接的,所以磁盘上只须保留一份副本,因此节约了磁盘空间。如果发现了bug或要升级也很简单,只要用新的库把原来的替换掉即可

C++和C的区别

包括但不限于:

  • C是面向过程的语言,C++是面向对象的语言,C++有“封装,继承和多态”的特性。封装隐藏了实现细节,使得代码模块化。继承通过子类继承父类的方法和属性,实现了代码重用。多态则是“一个接口,多个实现”,通过子类重写父类的虚函数,实现了接口重用。
  • C和C++内存管理的方法不一样,C使用malloc/free,C++除此之外还用new/delete
  • C++中还有函数重载和引用等概念,C中没有

C++三大特性:

  • 封装:突破了C语言对于函数的限制,封装可以隐藏实现细节,从而使得代码能够模块化。

    在c++中,封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。也就是说把成员变量和成员函数封装起来,对成员变量的访问只能通过成员函数。

  • 继承:继承机制是面向对象程序设计使代码进行复用的最重要的手段,他允许程序员在保证类原有类特性的基础上进行扩展来增加功能。原有的类称为基类或父类,产生的新类称为派生类或子类。

  • 多态:多态简单点说就是“一个接口,多种实现”,就是同一种事物表现出的多种形态。多态在面向对象语言中是指:接口多种的不同实现方式。也就是复用相同接口,实现不同操作。

模板的全特化与偏特化

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。

全特化的模板参数列表应当是空的

类模板:

template<typename T1, typename T2>
class Test
{
public:
	Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
	T1 a;
	T2 b;
};
 
// 全特化类模板
template<>
class Test<int , char>
{
public:
	Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
	int a;
	char b;
};

// 偏特化类模板
template <typename T2>
class Test<char, T2>
{
public:
	Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
	char a;
	T2 b;
};

//调用语句:
    int main(){
    	Test<double , double> t1(0.1,0.2);
	    Test<int , char> t2(1,'A');
	    Test<char, bool> t3('A',true);
        
        return 0;
}

函数模板

函数模板,只有全特化,不能偏特化

//模板函数
template<typename T1, typename T2>
void fun(T1 a , T2 b)
{
	cout<<"模板函数"<<endl;
}
 
//全特化
template<>
void fun<int ,char >(int a, char b)
{
	cout<<"全特化"<<endl;
}
 
//函数不存在偏特化:下面的代码是错误的
/*
template<typename T2>
void fun<char,T2>(char a, T2 b)
{
	cout<<"偏特化"<<endl;
}
*/

但函数允许重载,声明另一个函数模板即可替代偏特化的需要:

函数的特化:就是全特化

当模板遇到一些特殊的类型需要处理,不能直接使用当前的模板函数,需要对该类型特化出一个模板函数。

当使用一个判断相等的模板函数时

template<class T>
bool Isequal(T &p1,T &p2){
    return p1==p2;
}

但是该模板函数在对于字符串进行比较时就不能使用了,对于字符串我们不能直接比较,因此直接特化出一个专门供字符串使用的模板参数

tempalte<>//此处吧u添加类模板,直接使用空即可
bool Isequal<char*>(char* &p1,char* &p2)
{
    return strcmp(p1,p2)==0;
}

//实际为实现简单,可以直接写成
bool Isequal(char*& p1, char*& p2){
	cout << "char2" << endl;
	return strcmp(p1, p2) == 0;
}

注意:

  • 使用模板特化时,必须要先有基础的模板函数(就是上面第一个模板函数)
  • 使用特换模板函数时格式有要求:
    • template 后直接跟<> 里面不用写类型
    • 函数名<特化类型>(特化类型 参数1, 特化类型 参数2 , …) 在函数名后跟<>其中写要特化的类型

C语言实现多态

  • C语言中是没有class类这个概念的,但是有struct结构体,我们可以考虑使用struct来模拟
  • C语言的结构体内部是没有成员函数,如果实现这个父结构体和子结构体共有的函数呢,可以考虑使用函数指针来模拟。但是这样处理存在一个缺陷就是:父子各自的函数指针之间指向的不是类似C++中维护的虚函数表而是一块物理内存,如果模拟的函数过多的话就会不容易维护了。
//C实现动态,用到函数指针
typedef void(*FUN)();//定义一个函数制造行业内类型

struct Base
{
   FUN _f; 
}

//子类
struct Derived{
    Base _b;//子类中定义一个基类的对象,实现父类的继承
}

void FunB()
{
    printf("%s\n","Base::fun()");
}

void FUND(){
    printf("%s\n","Derived::fun()");
}

void Test2()
{
    Base b;//父类对象
    Derived d;//子类对象

    b._f = FunB;//父类对象调用父类同名函数
    d._b._f = FunD;//子类调用子类的同名函数

    Base *pb = &b;//父类指针指向父类对象
    pb->_f();

    pb = (Base *)&d;//让父类指针指向子类的对象,由于类型不匹配所以要进行强转
    pb->_f();
}

C++11新特性

nullptr

NULL是一个宏定义,在c和c++中的定义不同,c中NULL为(void*)0,而c++中NULL为整数0

//C语言中NULL定义
#define NULL (void*)0                //c语言中NULL为void类型的指针,但允许将NULL定义为0

//c++中NULL的定义
#ifndef NULL
#ifdef _cpluscplus                       //用于判定是c++类型还是c类型,详情看上一篇blog
#define NULL 0                         //c++中将NULL定义为整数0
#else
#define NULL ((void*)0)             //c语言中NULL为void类型的指针
#endif
#endif

在c++中int *p=NULL; 实际表示将指针P的值赋为0,而c++中当一个指针的值为0时,认为指针为空指针

为什么C++在NULL上选择不完全兼容C?

根本原因和C++的重载函数有关。C++通过搜索匹配参数的机制,试图找到最佳匹配(best-match)的函数,而如果继续支持void*的隐式类型转换,则会带来语义二义性(syntax ambiguous)的问题。

 // 考虑下面两个重载函数  
void foo(int i);  
void foo(char* p)  
  
foo(NULL); // which is called?

为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0

nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

当需要使用 NULL 时候,养成直接使用 nullptr的习惯。

类型推到

引入了auto和decltype关键字,让编译器确定变量的类型

auto不能用于函数传参,也不能推导数组类型

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的

decltype(表达式)

编译器分析表达式类型,不能计算吧表达式的值

范围for循环

初始化列表

模板增强

外部模板

传统 C++ 中,模板只有在使用时才会被编译器实例化。只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化

C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化

template class std::vector<bool>;            // 强行实例化
extern template class std::vector<double>;  // 不在该编译文件中实例化模板

类型别名模板

在传统 C++中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:

template< typename T, typename U, int value>
class SuckType {
public:
    T a;
    U b;
    SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法

C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:

template <typename T>
using NewType = SuckType<int, T, 1>;    // 合法

构造函数

  • 委托构造

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {  // 委托 Base() 构造函数
        value2 = 2;
    }
};
  • 继承构造

Lambda 表达式

新增容器

  • std::array

std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。

std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可:

std::array<int, 4> arr= {1,2,3,4};

int len = 4;
std::array<int, len> arr = {1,2,3,4}; // 非法, 数组大小参数必须是常量表达式
  • std::forward_list

std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。

无序容器

C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。

无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。

元组 std::tuple

元组的使用有三个核心的函数:

std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
std::tie: 元组拆包

#include <tuple>
#include <iostream>

auto get_student(int id)
{
    // 返回类型被推断为 std::tuple<double, char, std::string>
    if (id == 0)
        return std::make_tuple(3.8, 'A', "张三");
    if (id == 1)
        return std::make_tuple(2.9, 'C', "李四");
    if (id == 2)
        return std::make_tuple(1.7, 'D', "王五");
    return std::make_tuple(0.0, 'D', "null");   
    // 如果只写 0 会出现推断错误, 编译失败
}

int main()
{
    auto student = get_student(0);
    std::cout << "ID: 0, "
    << "GPA: " << std::get<0>(student) << ", "
    << "成绩: " << std::get<1>(student) << ", "
    << "姓名: " << std::get<2>(student) << '\n';

    double gpa;
    char grade;
    std::string name;

    // 元组进行拆包
    std::tie(gpa, grade, name) = get_student(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "成绩: " << grade << ", "
    << "姓名: " << name << '\n';
}

合并两个元组,可以通过 std::tuple_cat 来实现。

auto new_tuple = std::tuple_cat(get_student(1), std::move(t));

正则表达式

语言级线程支持

右值引用和move语义

C++11新特性之十:enable_shared_from_this

enable_shared_from_this是一个模板类,定义于头文件,其原型为:

template< class T > class enable_shared_from_this;

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权。

若一个类 T 继承 std::enable_shared_from_this ,则会为该类 T 提供成员函数: shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr 对象,它与 pt 共享 t 的所有权。

使用场合

当类A被share_ptr管理,且在类A的成员函数里需要把当前类对象作为参数传给其他函数时,就需要传递一个指向自身的share_ptr

1.为何不直接传递this指针

使用智能指针的初衷就是为了方便资源管理,如果在某些地方使用智能指针,某些地方使用原始指针,很容易破坏智能指针的语义,从而产生各种错误。

2.可以直接传递share_ptr么?

不能,因为这样会造成2个非共享的share_ptr指向同一个对象,未增加引用计数导对象被析构两次。例如:

#include <memory>
#include <iostream>
 
class Bad
{
public:
	std::shared_ptr<Bad> getptr() {
		return std::shared_ptr<Bad>(this);
	}
	~Bad() { std::cout << "Bad::~Bad() called" << std::endl; }
};
 
int main()
{
	// 错误的示例,每个shared_ptr都认为自己是对象仅有的所有者
	std::shared_ptr<Bad> bp1(new Bad());
	std::shared_ptr<Bad> bp2 = bp1->getptr();
	// 打印bp1和bp2的引用计数
	std::cout << "bp1.use_count() = " << bp1.use_count() << std::endl;
	std::cout << "bp2.use_count() = " << bp2.use_count() << std::endl;
}  // Bad 对象将会被删除两次

一个对象被删除两次会导致崩溃

正确的实现如下:

#include <memory>
#include <iostream>
 
struct Good : std::enable_shared_from_this<Good> // 注意:继承
{
public:
	std::shared_ptr<Good> getptr() {
		return shared_from_this();
	}
	~Good() { std::cout << "Good::~Good() called" << std::endl; }
};
 
int main()
{
	// 大括号用于限制作用域,这样智能指针就能在system("pause")之前析构
	{
		std::shared_ptr<Good> gp1(new Good());
		std::shared_ptr<Good> gp2 = gp1->getptr();
		// 打印gp1和gp2的引用计数
		std::cout << "gp1.use_count() = " << gp1.use_count() << std::endl;
		std::cout << "gp2.use_count() = " << gp2.use_count() << std::endl;
	}
	system("pause");
} 

在异步调用中,存在一个保活机制,异步函数执行的时间点我们是无法确定的,然而异步函数可能会使用到异步调用之前就存在的变量。为了保证该变量在异步函数执期间一直有效,我们可以传递一个指向自身的share_ptr给异步函数,这样在异步函数执行期间share_ptr所管理的对象就不会析构,所使用的变量也会一直有效了(保活)。

inline和define的特点和区别

内联函数inline

对于函数体很小、执行时间短但又频繁使用的函数,定义为内联函数提高函数调用的效率。内联函数在编译时将函数体嵌入到每个内联函数调用处。省去了参数传递、系统栈的保护与恢复等的时间开销

  • 内联函数以目标代码的增加为代价来换取时间的节省。

  • 内联函数在编译时被替换。

  • 内联函数一般不能含有循环语句和switch语句。

inline函数可以说是对编译器进行提出建议,是否最终成为内联函数做最后的替换操作编译器说了算,编译器有权拒绝

#define

预编译时进行简单的字符替换,不进行类型检查等操作,保存在预编译器的符号表中。

define和const的联系与区别

  • define定义的常量没有类型,只是简单的替换,可能有多个拷贝,占用的内存空间大,const定义的常量有类型,存放在静态存储区,只有一个拷贝,占用的内存空间小。
  • define定义的常量在预处理阶段替换,const在编译阶段确定它的值
  • define不会进行类型安全检查,而const会进行类型安全检查,安全性更高。
  • const可以定义函数而define不可以

class和struct的区别

C中的strcut不能有函数,但C++中可以。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!

  • 默认的继承访问权限. struct是public的,class是private的
    class B : public A就是为了指明是public继承,而不是用默认的private继承,若class B : A则是private继承
  • 定义模板参数. class这个关键字还用于定义模板参数,就像typename。但关键字struct不用于定义模板参数。

new/delete和malloc/free区别

malloc和calloc间的主要区别在于后者在返回指向内存的指针之前把它初始化为0。另一个区别是calloc的参数包括所需的元素的数量和每个元素的字节数

  • 属性不同. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c
  • 申请的内存所在位置. new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存.
  • 返回类型安全性. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
    而malloc内存分配成功则是返回void*类型,需要通过强制类型转换将空类型指针转换成我们需要的类型。
  • 是否调用构造函数/析构函数. new会先调用operator_ new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator_ delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
  • 内存分配失败时的返回值. new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
  • 是否需要指定内存大小. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的大小

const关键字

  • 修饰变量,说明该变量不可以被改变;
  • 修饰指针,分为指向常量的指针和指针常量;int *const p和const int *p
  • 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  • 修饰成员函数,说明该成员函数内不能修改成员变量,本质是const this指针。

在C++中,只有被声明为const的成员函数才能被一个const类对象调用

如果要声明一个const类型的类成员函数,只需要在成员函数列表后加上关键字const, 例如:

class Screen {
    public:
        char get() const;
};

若将成员函数声明为const,则不允许通过其修改类的数据成员。 值得注意的是,如果类中存在指针类型的数据成员即便是const函数只能保证不修改该指针的值,并不能保证不修改指针指向的对象

为什么对于类的const成员,只能使用初始化列表

主要是性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效

const放在函数前后的区别?

1、在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员,
只能发生读操作,不能发生写操作
2、在函数前面,返回值不可修改

static关键字

  • 变量:被static修饰的变量就是静态变量,它会在程序运行过程中一直存在,会被放在静态存储区。局部静态变量的作用域在函数体中,全局静态变量的作用域在这个文件里。
  • 函数:被static修饰的函数就是静态函数,静态函数只能在本文件中使用,不能被其他文件调用,也不会和其他文件中的同名函数冲突。
  • 类:而在类中,被static修饰的成员变量是类静态成员,这个静态成员会被类的多个对象共用。被static修饰的成员函数也属于静态成员,不是属于某个对象的,访问这个静态函数不需要引用对象名,而是通过引用类名来访问。

静态成员函数要访问非静态成员时,要用过对象来引用。局部静态变量在函数调用结束后也不会被回收,会一直在程序内存中,直到该函数再次被调用,它的值还是保持上一次调用结束后的值。

static 函数内不能访问非静态成员,而类的任何成员函数都可以访问类的静态数据成员

volatile关键字

  • 不可优化性. volatile 关键字是一种类型修饰符,用它声明的类型变量表示不可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • 易变性. volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)

explicit关键字

explicit关键字的作用就是防止对象间实现=赋值,防止类构造函数的隐式自动转换,类构造函数默认情况下即声明为implicit(隐式),另外explicit只用于单参数的构造函数,或者除了第一个参数以外的其他参数都有默认值.

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换

extern

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间

  • extern关键字表示其修饰的变量或函数的定义在其它源文件中。

    extern “C”表示接下来的内容按C的规则进行编译。比如C++编译器通过修改所有函数的名字实现函数重载机制,C则是按函数原本的名字进行编译的。

restrict

restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

override与final

  • override:表示当前函数重写了基类的虚函数

  • final关键字:使派生类不可覆盖基类的虚函数

全局对象:在程序启动被创建,在程序结束时销毁
局部自动对象:进入其所在的程序块被创建,在离开块时销毁
局部static对象:在第一次使用时分配,在程序结束时销毁
动态分配对象:显示被释放,对象才销毁

typedef介绍

使用typedef定义新类型的方法:在传统的变量声明表达式里用**(新的)类型名替换变量名**,然后把关键字typedef加在该语句的开头就行了。

定义函数指针类型

定义函数指针变量

//定义了一个函数指针变量pFunc,它可以指向这样的函数:返回值为int,形参为char*、int
int (*pFunc)(char *frame, int len);
定义了5个函数指针变量:pFunc[0]、pFunc[1]···,它们都可以指向这样的函数:返回值为int*,形参为int
typedef  int (*pFunc_t)(char *frame, int len);//定义了一个类型pFunc_t
typedef  int (*pFunc_t)(char *frame, int len);//定义了一个类型pFunc_t
 
int read_voltage(char *data, int len)
{
    int voltage = 0;
    ···//其他功能代码
 
    return voltage;
}
int main(void)
{
    pFunc_t   pHandler = read_voltage;//使用类型pFunc_t来定义函数指针变量
    ···//其他功能代码
}

定义数组指针类型

1、定义数组指针变量(注意数组指针、指针数组的区别)

  • int(*pArr)[5];//定义了一个数组指针变量pArr,pArr可以指向一个int [5]的一维数组

  • char(*pArr)[4][5];///定义了一个数组指针变量pArr,pArr可以指向一个char[4][5]的二维数组

int(*pArr)[5];//pArr是一个指向含5个int元素的一维数组的指针变量
int a[5] = {1,2,3,4,5};
int b[6] = {1,2,3,4,5,6};
pArr = &a;//完全合法,无警告
pArr = a;//发生编译警告,赋值时类型不匹配:a的类型为int(*),而pArr的类型为int(*)[5]
pArr = &a[0];//发生编译警告,赋值时类型不匹配:a的类型为int(*),而pArr的类型为int(*)[5]
pArr = &b;//发生编译警告,赋值时类型不匹配:&b的类型为int(*)[6],而pArr的类型为int(*)[5]
pArr = (int(*)[5])&b;//类型强制转换为int(*)[5],完全合法,无警告

2、定义数组指针类型

如同上面定义函数指针类型的方法,直接在前面加typedef即可,例如

typedef int (*pArr_t)[5];//定义了一个指针类型pArr_t,该类型的指针可以指向含5个int元素的数组

typedef int(*pArr_t)[5];//定义一个指针类型,该类型的指针可以指向含5个int元素的一维数组
 
int main(void)
{
    int a[5] = {1,2,3,4,5};
    int b[6] = {1,2,3,4,5,6};
    pArr_t pA;//定义数组指针变量pA
    pA= &a;//完全合法,无警告    
    pA= (pArr_t)&b;//类型强制转换为pArr_t,完全合法,无警告
}

定义数组类型

如果我们想声明一个含5个int元素的一维数组,一般会这么写:int a[5];

如果我们想声明多个含5个int元素的一维数组,一般会这么写:int a1[5], a2[5], a3[5]···,或者 a[N][5]

可见,对于定义多个一维数组,写起来略显复杂,这时,我们就应该把数组定义为一个类型,例如:

typedef int arr_t[5];//定义了一个数组类型arr_t,该类型的变量是个数组

typedef int arr_t[5];
int main(void)
{
    arr_t d;        //d是个数组,这一行等价于:  int d[5];
    arr_t b1, b2, b3;//b1, b2, b3都是数组
    
    d[0] = 1;
    d[1] = 2;
    d[4] = 134;
    d[5] = 253;//编译警告:下标越界
}

给已定义的变量类型起个别名

typedef unsigned char uin8_t;       //uint8_t就是unsigned char的别名
struct __person
{
    char    name[20];
    uint8_t age;
    uint8_t height;
}
typedef __person person_t;
//以上两段代码也可合并为一段,如下:
typedef struct __person
{
    char    name[20];
    uint8_t age;
    uint8_t height;
}person_t;

作用是给struct __person起了个别名person_t

size_t和int

size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关。

在32位架构中被普遍定义为:

typedef unsigned int size_t;

而在64位架构中被定义为:

typedef unsigned long size_t;
size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。

为什么有时候不用int,而是用size_type或者size_t:

​ 与int固定四个字节不同有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int. 使用Int既有可能浪费,又有可能范围不够大。

指针常量与常量指针

int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量

常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。

指针和引用的区别?

  • 指针是一个新的变量,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
  • 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)
  • 有多级指针,但是没有多级引用,只能有一级引用。
  • 指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
  • sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小(4字节)。
  • 引用访问一个变量是直接访问,而指针访问一个变量是间接访问

在函数参数传递的时候,什么时候使用指针,什么时候使用引用?

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

  • 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要开辟内存,开销小

  • 类对象作为参数传递的时候使用引用

    由于按值传递来传递类对象参数时将调用复制构造函数,所以应该用引用传递对象,这样可以节省调用构造函数函数的时间和存储新对象的空间

栈和堆的区别

  • 管理方式不同
    • 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
  • 空间大小不同
    • 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M
  • 能否产生碎片不同
    • 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
  • 生长方向不同
    • 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长

堆快还是栈快

栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

4种强制转换

C++中强制转换为static_cast, dynamic_cast,const_cast, reinterpret_cast

  • static_cast:用于各种隐式转换。具体的说,就是用户各种基本数据类型之间的转换,比如把int换成char,float换成int等。以及派生类(子类)的指针转换成基类(父类)指针的转换。

    特性与要点:

    • 它没有运行时类型检查,所以是有安全隐患的。

    • 在派生类指针转换到基类指针时,是没有任何问题的,在基类指针转换到派生类指针的时候,会有安全问题。

    • static_cast不能转换const,volatile等属性

  • dynamic_cast:

    用于动态类型转换。具体的说,就是在基类指针到派生类指针,或者派生类到基类指针的转换。
    dynamic_cast能够提供运行时类型检查,只用于含有虚函数的类。
    dynamic_cast如果不能转换返回NULL

  • const_cast:用于去除const常量属性,使其可以修改 ,也就是说,原本定义为const的变量在定义后就不能进行修改的,但是使用const_cast操作之后,可以通过这个指针或变量进行修改; 另外还有volatile属性的转换

  • reinterpret_cast
    几乎什么都可以转,用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不够安全

没有显示类型转换的转换都是隐式转换,如内置类型低精度的变量赋值给高精度的变量会发生隐式转换,如传给一个类的构造函数的变量若不是该类构造函数规定的参数类型的话也可能发生隐式转换。

野指针

野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

  • 指针变量未初始化
int *p;
scanf("%d",p);
  • 动态创建内存free掉后,没有把指针指向NULL
int *p=(int *)malloc(sizeof(int));
free(p);
  • 指针超出了变量的作用范围
    当你在调用函数时,返回一个指向栈内存的指针时,因为当函数结束时,栈帧回退,栈内存被删除

智能指针

智能指针的内存泄漏如何解决

为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

内存泄漏

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用

shared_ptr都有一个关联的计数器,称其为引用计数

  • 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针
  • 每次创建类的新对象时,初始化指针并将引用计数置为1
  • 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
  • 当给shared_ptr赋值,会递减左边所指对象的引用计数,递增右边指向对象的引用计数
  • 调用析构函数时,构造函数减少引用计数,一旦一个shared_ptr的计数器变为0,它就会自动销毁对象释放占用内存

unique_ptr:
与shared_ptr不同,保证同一时间内只有一个智能指针可以指向该对象,当unique_ptr被销毁,它所指的对象也被销毁

将unique_ptr转换为shared_ptr

std::unique_ptr<std::string> name= std::make_unique<std::string>("zhang");
std::shared_ptr<std::string> name2= std::move(name);

weak_ptr:
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也被释放

make_shared<T> (args) 	//返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p (q) 	    //p是shared_ptr的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q 	                //p和q都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数
p.unique() 	            //若p.use_count()为1,返回true;否则返回false
p.use_count() 	        //返回与p共享对象的智能指针数量

shared_ptr会递减它所指对象的引用计数,如果引用计数变为0,shared_ptr的析构函数会销毁对象,并释放占用内存

谨慎使用裸指针

https://blog.csdn/u011475134/article/details/76785290

当需要裸指针与智能指针搭配使用时,需要避免如下操作:

  • 使用裸指针初始化多个智能指针
  • 对智能指针使用的裸指针执行delete操作

以上操作会导致程序再次尝试销毁已被销毁了的对象,进而造成程序崩溃。所以,当使用裸指针初始化智能指针后,应确保裸指针永远不应该被再次使用

#include <iostream>
#include <memory>
using namespace std;

class A{
public:
    string id;
    A(string id):id(id){cout<<id<<":构造函数"<<endl;}
    ~A(){cout<<id<<":析构函数"<<endl;}
};

int main() {
    A *pa = new A("ptrA");
    unique_ptr<A> unique_pa(pa);
//    delete pa;  // 运行错误

    A *pb = new A("ptrB");
    unique_ptr<A> unique_pb1(pb);
//    unique_ptr<A> unique_pb2(pb);  // 运行错误
    return 0;
}

不要使用静态分配对象的指针初始化智能指针

不要使用静态分配对象的指针初始化智能指针,否则,当智能指针本身被撤销时,它将试图删除指向非动态分配对象的指针,导致未定义的行为。

#include <iostream>
#include <memory>
using namespace std;

class A{
public:
    string id;
    A(string id):id(id){cout<<id<<":构造函数"<<endl;}
    ~A(){cout<<id<<":析构函数"<<endl;}
};

A a("全局变量");

int main() {
    A b("局部变量");
//    unique_ptr<A> pa(&a); // 运行错误
    unique_ptr<A> pa(&b);
    return 0;
}

运行结果:

全局变量:构造函数
局部变量:构造函数
局部变量:析构函数
局部变量:析构函数
全局变量:析构函数

allocator类

  • new由一些灵活性的局限,new将内存分配和对象构造组合在一起,delete将对象析构和内存释放组合在一起
  • allocator类定义在头文件memory中,将内存分配和对象构造分离开来。allocator类允许我们先分配一块大的内存,需要时执行对象创建操作

C/C++内存泄露及其检测工具

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该 内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

造成内存泄漏的几种原因:

  • 类的构造函数和析构函数中new和delete没有配套

  • 在释放对象数组时没有使用delete[],使用了delete

  • 没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

  • 没有正确的清楚嵌套的对象指针

检测内存泄漏

检测内存泄漏的基本原理:检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。

linux下检测工具:

参考:

  • Valgrind工具集

Memcheck

最常用的工具,用来检测程序中出现的内存问题,所有对内存的读写都会被检测到,一切对malloc、free、new、delete的调用都会被捕获

valgrind --tool=memcheck --leak-check=full ./test

将程序编译生成可执行文件后执行:valgrind --leak-check=full ./程序名

C++所有的构造函数

C++中的构造函数主要有三种类型:默认构造函数、重载构造函数和拷贝构造函数

  • 默认构造函数是当类没有实现自己的构造函数时,编译器默认提供的一个构造函数。
  • 重载构造函数也称为一般构造函数,一个类可以有多个重载构造函数,但是需要参数类型或个数不相同。可以在重载构造函数中自定义类的初始化方式。
  • 拷贝构造函数是在发生对象复制的时候调用的。

左值与右值引用

C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法

  • 可以取地址的,有名字的,非临时的就是左值;
  • 不能取地址的,没有名字的,临时的就是右值;

结构体内存对齐方式

由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来。

类中非静态成员变量的大小与编译器内存对齐的设置有关。

成员变量在类中的内存存储并不一定是连续的。它是按照编译器的设置,按照内存块来存储的,这个内存块大小的取值,就是内存对齐。

  • 类大小的计算遵循结构体的对齐原则

    linux对齐模数为4,vs 中的默认值为8

    对齐规则是按照成员的声明顺序,依次安排内存,其偏移量为成员大小的整数倍
    最后结构体的大小为最大成员的整数倍

  • 类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响

  • 虚函数对类的大小有影响,是因为虚函数表指针带来的影响

  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响

  • 空类的大小是一个特殊情况,空类的大小为1

静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区。

什么情况下会调用拷贝构造函数(三种情况)

  • 对象以值传递的方式传入函数参数

void func(Dog dog){};

  • 对象以值传递的方式从函数返回

Dog func(){ Dog d; return d;}

  • 对象需要通过另外一个对象进行初始化

拷贝构造函数的参数必须加const,因为防止修改,本来就是用现有的对象初始化新的对象。

深拷贝和浅拷贝

只有当对象的成员属性在堆区开辟空间内存时,才会涉及深浅拷贝,如果仅仅是在栈区开辟内存,则默认的拷贝构造函数和析构函数就可以满足要求。

  • 浅拷贝:使用默认拷贝构造函数,拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标,因此涉及堆区开辟内存时,会将两个成员属性指向相同的内存空间,从而在释放时导致内存空间被多次释放,使得程序down掉。
  • 浅拷贝的问题:当出现类的等号赋值时,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次free函数,指向的内存空间已经被释放掉,再次free会报错;另外,一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了所以,这时,必须采用深拷贝
  • 深拷贝:自定义拷贝构造函数,在堆内存中另外申请空间来储存数据,从而解决指针悬挂的问题。需要注意自定义析构函数中应该释放掉申请的内存

我们在定义类或者结构体,这些结构的时候,最后都重写拷贝函数,避免浅拷贝这类不易发现但后果严重的错误产生

对象复用的了解,零拷贝的了解

对象复用指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。

零拷贝指的是在进行操作时,避免CPU从一处存储拷贝到另一处存储。在Linux中,我们可以减少数据在内核空间和用户空间的来回拷贝实现,比如通过调用mmap()来代替read调用。

用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去

友元全局函数、友元类、友元成员函数

友元主要是为了访问类中的私有成员(包括属性和方法),会破坏C++的封装性,尽量不使用

  • 友元全局函数

    • 友元函数声明可以在类中的任何地方,一般放在类定义的开始或结尾
    • 一个函数可以是多个类的友元函数,只需要在各个类中分别声明
    • 友元函数在类内声明,类外定义,定义和使用时不需加作用域和类名,与普通函数无异。
  • 友元类

    • 友元不可继承
    • 友元是单向的,类A是类B的友元类,但类B不一定是类A的
    • 友元不具有传递性,类A是类B的友元类,类B是类C的友元类,但类A不一定是类C的友元类。
  • 友元成员函数

    • 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员

函数指针与指针函数

  • 指针函数int* f(int x, int y)本质是函数,返回值为指针,函数指针int (*f)(int x)本质是指针,指向函数的指针

  • 通常我们可以将指针指向某类型的变量,称为类型指针(如,整型指针)。若将一个指针指向函数,则称为函数指针。

  • 函数名代表函数的入口地址,同样的,我们可以通过根据该地址进行函数调用,而非直接调用函数名。

void test001(){
    printf("hello, world");
}

int main(){
    void(*myfunc)() = test001;//将函数写成函数指针
    myfunc(); //调用函数指针 hello world
}

test001的函数名与myfunc函数指针都是一样的,即都是函数指针。test001函数名是一个函数指针常量,而myfunc是一个函数指针变量,这是它们的关系。

  • 函数指针多用于回调函数,回调函数最大的优势在于灵活操作,可以实现用户定制的函数,降低耦合性,实现多样性,如STL中

什么是操作系统的原子操作

原子操作是不可分割的,在执行完毕前不会被任何其它任务或事件中断:

  • 在单线程中, 能够在单条指令中完成的操作都可以认为是原子操作,不能在单条指令中完成的操作也都可以认为不是原子操作,因为中断可以且只能发生于指令之间;
  • 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作

i++和++i实现

C++内置类型的后置++返回的是变量的拷贝,也就是不可修改的值;前置++返回的是变量的引用,因此可以作为修改的左值。即++(++a)或(++a)++都可以,但++(a++)不可以,(C++默认必须修改a的值,如果不修改则报错)。

可见,无论是 ++i 还是 i++ ,都是使用三个指令来实现的:

  • 将值从内存拷贝到寄存器;
  • 寄存器自增;
  • 将值从寄存器拷贝到内存;

i++的操作分三步:

  • 栈中取出i

  • i自增1

  • 将i存到栈

所以i++不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致i的值不正确自增

++i

  • 在多核的机器上,cpu在读取内存i时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。
//++i
int&  int::operator++()
{
    *this +=1;
    return *this;
}

//i++,注意后置++有占位参数以区分跟前置++不同
const int  int::operator++(int)
{
    int oldValue = *this;
    ++(*this);
    return oldValue;
}

多继承和菱形继承

多继承

多继承会产生二义性的问题。如果继承的多个父类中有同名的成员属性和成员函数,在子类调用时,需要指定作用域从而确定父类。

菱形继承

两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类,这种继承称为菱形继承。

菱形继承会产生问题
  • **浪费空间。**另一个类有继承2分父类,但只需要一份即可
  • 二义性。从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题。

解决菱形继承的问题

使用虚继承,在继承方式前加virtual,这样操作的是共享的一份数据

  • 虚继承

    • 一般通过虚基类指针和虚基类表实现,将共同基类设置为虚基类
    • **每个虚继承的子类(虚基类本身没有)**都有一个虚基类指针(占用一个指针的存储空间)和虚基类表(不占用类对象的存储空间),虚基类指针属于对象,虚基类表属于类

    使用成员名限定可以消除二义性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-THsRQHTF-1649153548363)(/home/xiaohu/.config/Typora/typora-user-images/image-20200802180701977.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aKPGMWxi-1649153548364)(/home/xiaohu/.config/Typora/typora-user-images/image-20200802180841752.png)]

重载,重写(覆盖)和隐藏的区别

  • 重载(overload):是函数名相同,参数列表不同。重载只是在同一个类的内部存在,但是不能靠返回类型来判断
  • 重写:指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
  • 隐藏:隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏

virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有

隐藏和重写,重载的区别:

(1)与重载范围不同:隐藏函数和被隐藏函数在不同类中。

(2)参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。

运算符重载总结

  • 重载运算符(),[] ,->, =的时候,运算符重载函数必须声明为类的成员函数
  • 重载运算符<<,>>的时候,运算符只能通过全局函数配合友元函数进行重载
  • 不要重载&&和||运算符,因为无法实现短路原则。

构造函数为什么不能是虚函数,而析构函数是虚函数

构造函数为什么不能是虚函数

  • 从存储空间角度: 虚函数对应一个vtable,vtable其实是存储在对象的内存空间。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
  • 从使用角度:虚函数可以使函数重载得到调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数
  • 从实现上看:vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数

为什么析构函数必须是虚函数

因为当发生多态时,父类指针在堆上创建子类对象,销毁时会内存泄漏

  • 仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数;
  • 当发生多态时(父类指针或引用指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。

首先析构函数可以为虚函数,当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全造成内存泄漏。

构造函数或者析构函数中调用虚函数会怎样

在构造函数中调用虚函数,由于当前对象还没有构造完成,此时调用的虚函数指向的是基类的函数实现方式。

在析构函数中调用虚函数,此时调用的是子类的函数实现方式。

怎么理解多态和虚函数

C++多态虚函数表详解(多重继承、多继承情况)https://blog.csdn/qq_36359022/article/details/81870219?utm_medium=distribute.pc_relevant.none-task-blog-baidulandingword-2&spm=1001.2101.3001.4242

https://blog.csdn/primeprime/article/details/80776625

多态的实现分成两种:

  • 编译时的多态,主要是通过函数重载和运算符重载来实现的,是通过静态联编实现的

  • 运行时多态,主要是通过函数覆盖来实现的,它需要满足3个条件:

    • 基类函数必须是虚函数,
    • 基类的指针或引用指向子类的时候,
    • 当子类中对原有的虚函数进行重写后就
      会形成多态;它是通过动态联编实现的
      运行时的多态可以让基类的指针或引用指向不同的对象的时候表现出来不同的特性;

  • 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

虚函数的实现:在有虚函数的类中,创建对象,编译器在对象的内存结构头部添加一个虚函数表指针,虚函数表指针指向对象所属类的虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重写新的函数地址。使用了虚函数,会增加访问内存开销,降低效率

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针

  • 虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

  • 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

  • 实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,虚函数表(vftable)属于类,或者说这个类的所有对象共享一个虚函数表;虚函数指针(vfptr)属于单个对象

  • 虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。

  • 编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心调用的函数是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr—>虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。

抽象类和纯虚函数

在程序设计中,如果仅仅为了设计一些虚函数接口,打算在子类中对其进行重写,那么不需要在父类中对虚函数的函数体提供无意义的代码,可以通过纯虚函数满足需求。

  • 纯虚函数的语法格式:virtual 返回值类型 函数名 () = 0; 只需要将函数体完全替换为 =0即可,纯虚函数必须在子类中进行实现,在子类外实现是无效的。

  • 注意

    • 包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象
    • 但纯虚析构函数例外,因为子类不会继承父类的析构函数

    使用场景:当这个类本身产生一个实例没有意义的情况下,把这个类的函数实现为纯虚函数,比如动物可以派生出老虎兔子,但是实例化一个动物对象就没有意义。并且可以规定派生的子类必须重写某些函数的情况下可以写成纯虚函数。

纯虚析构

  • 纯虚析构需要类内声明,类外实现
  • 纯虚析构也是虚函数,该类也为抽象类
  • 子类不会继承父类的析构函数,当父类纯虚析构没有实现时,子类不是抽象类,可以创建创建对象。

类模板和函数模板

通过template或template实现,主要用于数据的类型参数化,简化代码,有类模板和函数模板,函数模板是用于生成函数的,类模板则是用于生成类的

  • 类模板和函数模板定义
  • template声明下面是函数定义,则为函数模板,否则为类模板。
  • 注意:每个函数模板前必须有且仅有一个template声明,不允许多个template声明后只有一个函数模板,也不允许一个template声明后有多个函数模板(类模板同理)。

编译器会对函数模板进行两次编译:第一次编译在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法检查,并不会生成具体的代码。第二次编译时对代码进行参数替换后再进行编译,生成具体的函数代码。

只在堆上/栈上

  • 类模板与函数模板的区别
    • 类模板不支持自动类型推导,数据类型可以有默认参数.

      创建对象

  • 只能在堆上生成对象:将析构函数设置为私有。
    原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
  • 只能在栈上生成对象:将new 和 delete 重载为私有。
    原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。
    将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

类只能在堆上创建对象

  • 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  • 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

注意

  1. 在堆和栈上创建对象都会调用构造函数,为了防止在栈上创建对象我们将构造函数私有化。
  2. 拷贝构造函数是在栈上创建对象。
class HeapOnly
{
public:
    static HeapOnly* CreateObject()
    {
        return new HeapOnly;//这使得创建该类对象都只能通过new,确保了该类只能在堆上创建对象
    }

private:
    HeapOnly(){}//在堆和栈上创建对象都会调用构造函数,为了防止在栈上创建对象我们将构造函数私有化
    HeapOnly(const HeapOnly&) = delete;//拷贝构造函数是在栈上创建对象
};

类只能在栈上创建对象

方法一:

  • 将类的构造函数私有。防止在堆上创建对象
  • 提供一个静态的成员函数,在该静态成员函数中完成栈对象的创建

注意

  1. 在堆和栈上创建对象都会调用构造函数,为了防止在堆上创建对象,应该将构造函数私有化。
 1 class StackOnly
 2 {
 3 public:
 4     static StackOnly CreateObject()
 5     {
 6         return StackOnly();//确保了该类创建对象时不会使用new操作符,从而使得该类只能在栈上创建对象
 7     }
 8 private:
 9     StackOnly(){}//在堆和栈上创建对象都会调用构造函数,为了防止在堆上创建对象,应该将构造函数私有化
10 };

方法二 只能在栈上创建对象,即不能在堆上创建,因此只要将new的功能屏蔽掉即可,即屏蔽掉operator new和定位new表达式,注意:屏蔽了operator new,实际也将定位new屏蔽掉。

class StackOnly    
{    
public:        
    StackOnly()  {} 
private:        
    void* operator new(size_t size);    
    void operator delete(void* p); 
};  

注意

  • new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
  • operator new函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试 执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
  • operator delete函数最终是通过free来释放空间的

静态链表和动态链表区别:

静态链表和动态链表是线性表链式存储结构的两种不同的表示方式。

  • 静态链表是用类似于数组方法实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配地址空间大小。所以静态链表的初始长度一般是固定的,在做插入和删除操作时不需要移动元素,仅需修改指针。

  • 动态链表是用内存申请函数(malloc/new)动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过指针来顺序访问。

静态链表

结构体中的成员可以是各种类型的指针变量,当一个结构体中有一个或多个成员的基类型是本结构体类型时,则称这种结构体为“引用自身的结构体”。如:

struct node
{
  char ch;
   int num;
  struct node *p;
};
 
struct node a;	//声明一个结构体变量

迭代器与指针的差别

迭代器:
(1)迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->,++ --等封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等作;
(2)迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
(3)在设计模式中有一种模式叫迭代器模式,简单来说就是提供一种方法,在不需要暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素,这种设计思维在STL中得到了广泛的应用,是STL的关键所在,通过迭代器,容器和算法可以有机的粘合在一起,只要对算法给予不同的迭代器,就可以对不同容器进行相同的操作。

ite=find(vec.begin(),vec.end(),88);
vec.insert(ite,2,77);  //迭代器标记的位置前,插入数据;
cout<<*ite<<endl;  //会崩溃,因为迭代器在使用后就释放了,*ite的时候就找不到它的地址了;

注:迭代器在使用后就释放了,不能再继续使用,但是指针可以!!

指针:
指针能指向函数而迭代器不行,迭代器只能指向容器;指针是迭代器的一种。指针只能用于某些特定的容器;迭代器是指针的抽象和泛化。所以,指针满足迭代器的一切要求。
总之,指针和迭代器是有很大差别的,虽然他们表现的行为相似,但是本质是不一样的!一个是类模板,一个是存放一个家伙的地址的指针变量。

析构顺序

局部对象,在退出程序块时析构

静态对象,在定义所在文件结束时析构

全局对象,在程序结束时析构

继承对象,先析构派生类,再析构父类

对象成员,先析构类对象,再析构对象成员

#include <iostream>
using namespace std;

class A{
public:
    A(){
        cout<<"构造A"<<endl;
    }
    ~ A(){
        cout<<"析构A"<<endl;
    }
};

class B{
public:
    B(){
        cout<<"构造B"<<endl;
    }
    ~ B(){
        cout<<"析构B"<<endl;
    }

};

class C{
public:
    C(){
        cout<<"构造C"<<endl;
    }
    ~ C(){
        cout<<"析构C"<<endl;
    }
};

class D{
public:
    D(){
        cout<<"构造D"<<endl;
    }
    ~ D(){
        cout<<"析构D"<<endl;
    }
};

C c;
int  main() {
    A *pa=new A();
    B b;
    static D d;
    delete pa;
}

/*
构造C
构造A
构造B
构造D
析构A
析构B
析构D
析构C
 */

头文件避免重复引用

#ifndef,#define,#endif是C/C++语言中的宏定义,通过宏定义避免文件多次编译

条件指示符#ifndef检查预编译常量在前面是否已经被定义,如果在前面没有被定义,则条件指示符的值为真,于是从#ifndef到#endif之间的所有语句都被include进来进行处理

相反,如果#ifndef指示符的值为假,则#ifndef与#endif指示符之间的行将被忽略。

#ifndef FOO_H
#define FOO_H
Rest of header here...

#endif // FOO_H

#pragma once是一个比较常用的C/C++预处理指令,只要在头文件的最开始加入这条预处理指令,就能够保证头文件只被编译一次。

作者:小谷围coder
链接:https://www.nowcoder/discuss/489210
来源:牛客网

为什么C++没有实现垃圾回收?

  • 首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。
  • 垃圾回收会使得C++不适合进行很多底层的操作。

本文标签: