C++浓缩学习笔记(1)-语言基础

文章主要引用:

阿秀的学习笔记 (interviewguide.cn)

牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)

看过什么书(百度)

C++ Primer Plus, 代码随想录,清华大学教材;

计算机网络:计算机网络 - 自顶向下方法,图解HTTP,图解TCP/IP ;

操作系统:现代操作系统,深入理解计算机系统

一、C++特点配置

1.1 特点

简述下C++语言的特点

参考回答

  1. C++在C语言基础上引入了面对对象的机制,同时也兼容C语言

  2. C++有三大特性(1)封装。(2)继承。(3)多态;

  3. C++语言编写出的程序结构清晰、易于扩充,程序可读性好

  4. C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;

  5. C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;

  6. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。

  7. 同时,C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。

说说C语言和C++的区别

参考回答

  1. C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。

  2. C++是面对对象的编程语言;C语言是面对过程的编程语言。

  3. C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量引用、cast转换、智能指针、try—catch等等;

  4. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用

ASCLL

数字的0-9对应ASCII码的48-57

大写字母的A-Z对应ASCII码的65-90

小写字母的a-z对应ASCII码的97-122

1.2 配置

说说include头文件的顺序以及双引号""和尖括号<>的区别

参考回答

  1. 区别:

    (1)尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件

    (2)编译器预处理阶段查找头文件的路径不一样。

  2. 查找路径:

    (1)使用尖括号<>的头文件的查找路径:编译器设置的头文件路径-->系统变量。

    (2)使用双引号""的头文件的查找路径:当前头文件目录-->编译器设置的头文件路径-->系统变量。

C++头文件<bits/c++std.h>详解——万能头文件

优点:

  1. 可以节省大量写和搜头文件的时间。

  2. 非常简洁,只需要一行代码就够了。

缺点:

  1. 包含的头文件太多,编译起来很费时间。

  2. 在一些老版本的编译软件里可能并没有该头文件。

二、C++语法

2.1 结构体与类

说说 C++中 struct 和 class 的区别

参考回答

  1. struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装

  2. struct 中默认的访问权限是 public 的(也可以说没有访问权限的设定),而 class 中默认的访问控制权限是 private

  3. class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数

答案解析

  1. C++ 中的 struct 是对 C 中的 struct 进行了扩充,它们在声明时的区别如下:

    C C++
    成员函数 不能有 可以
    静态成员 不能有 可以
    访问控制 默认public,不能修改 public/private/protected
    继承关系 不可以继承 可从类或者其他结构体继承
    初始化 不能直接初始化数据成员 可以
  2. 使用时的区别:C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名,而 C++ 中可以省略 struct 关键字直接使用,例如:

    struct Student{  int  iAgeNum;  string strName; } 
    typedef struct Student Student2; //C中取别名
    struct Student stu1; // C 中正常使用 
    Student2 stu2;   // C 中通过取别名的使用 
    Student stu3;   // C++ 中使用

结构体可以直接赋值吗

可以。

声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值;

注意:当有多个指针指向同一段内存时,某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前一定要确保其他指针不再使用这段内存空间。

2.2 代码执行

简述C++从代码到可执行二进制文件的过程

 参考回答

​ C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接

答案解析

  1. 预编译:这个过程主要的处理操作如下:

    (1) 将所有的#define删除,并且展开所有的宏定义

    (2) 处理所有的条件预编译指令,如#if、#ifdef

    (3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。

    (4) 过滤注释

    (5) 添加行号和文件名标识

  2. 编译:这个过程主要的处理操作如下:

    (1) 词法分析:将源代码的字符序列分割成一系列的记号。

    (2) 语法分析:对记号进行语法分析,产生语法树。

    (3) 语义分析:判断表达式是否有意义。

    (4) 代码优化:

    (5) 目标代码生成:生成汇编代码。

    (6) 目标代码优化:

  3. 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。

  4. 链接:将不同的源文件产生的目标文件进行接,从而形成一个可以执行的程序。

    链接分为静态链接和动态链接。

静态链接与动态链接

静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

优点 :程序在发布时不需要依赖库,可以独立执行;

缺点:在于程序的体积会相对较大,会出现同一个目标文件都在内存存在多个副本。而且如果静态库更新之后,所有可执行文件需要重新链接;

动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

优点:多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,更新方便:更新时只需要替换原来的目标文件。

缺点:在于由于运行时加载,可能影响程序的前期执行性能。

用dll中的什么,接口和函数的区别

DLL 是一个包含可由多个程序同时使用的代码和数据的库。 每个程序都可以使用某个 DLL 中包含的功能来某种实现操作。 这有助于促进代码重用和内存的有效使用。好处是程序不需要在运行之初加载所有代码,只有在程序需要某个函数的时候才从DLL中取出。

函数是语句序列的打包;
方法是对 对象成员的操作,由函数实现;
接口是对方法的抽象和概括,由方法实现具体的接口;

http://t.csdn.cn/a1ayS

条件编译

#ifdef、#else、#endif和#ifndef

一般情况下,在进行编译时对源程序中的每一行都要编译。但是有时希望程序中某一部分内容只在满足一定条件时才进行编译,也就是指定对程序中的一部分内容进行编译的条件。如果不满足这个条件,就不编译这部分内容。这就是“条件编译”。

利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。

虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长(因为所有if语句都编译),运行时间长(因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。

2.3 语法

浮点数怎么保存(诺瓦)

1000.101 这种二进制数,规格化表示成 1.000101 x 2^3,其中,最为关键的是 000101 和 3 这两个东西,它就可以包含了这个二进制小数的所有信息:

  • 000101 称为尾数,即小数点后面的数字;
  • 3 称为指数,指定了小数点在数据中的位置;

现在绝大多数计算机使用的浮点数,一般采用的是 IEEE 制定的国际标准,这种标准形式如下图:

这三个重要部分的意义如下:

  • 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
  • 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大
  • 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;

用 32 位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float 变量,而用 64 位来表示的浮点数,称为双精度浮点数,也就是 double 变量,它们的结构如下:

可以看到:

  • double 的尾数部分是 52 位,float 的尾数部分是 23 位,由于同时都带有一个固定隐含位(这个后面会说),所以 double 有 53 个二进制有效位,float 有 24 个二进制有效位,所以所以它们的精度在十进制中分别是 log10(2^53) 约等于 15.95 和 log10(2^24) 约等于 7.22 位,因此 double 的有效数字是 15~16 位,float 的有效数字是 7~8 位,这些有效位是包含整数部分和小数部分;
  • double 的指数部分是 11 位,而 float 的指数位是 8 位,意味着 double 相比 float 能表示更大的数值范围;

sizeof 和strlen 的区别

  1. sizeof是一个操作符,strlen是库函数

  2. 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度

  3. sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数

导入C函数的关键字是什么,C++编译时和C有什么不同?

参考回答

  1. 关键字:在C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

  2. 编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名

答案解析

//extern示例 //在C++程序里边声明该函数,会指示编译器这部分代码按C语言的进行编译 extern "C" int strcmp(const char *s1, const char *s2);  //在C++程序里边声明该函数 extern "C"
{     #include <string.h>//string.h里边包含了要调用的C函数的声明 }  //两种不同的语言,有着不同的编译规则,比如一个函数fun,可能C语言编译的时候为_fun,而C++则是__fun__

变量的声明和定义的区别

变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。一个变量可以在多个地方声明, 但是只在一个地方定义。

注意:加入extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

普通的函数和成员函数的区别?

普通函数是在类的外部定义的,而成员函数是在类的内部定义的。

普通函数不能直接访问类的私有(private)和保护(protected)成员,而成员函数可以访问类的所有成员,包括私有和保护成员。

普通函数可以直接调用,而成员函数需要通过类的对象来调用。

成员函数有一个特殊的指针this,它指向调用该成员函数的对象。普通函数没有这个指针。

this 指针是干嘛的?

this 指针是指向当前对象的地址。主要用于在类的成员函数中访问当前对象的成员变量和成员函数。

当一个对象调用自己的成员函数时,编译器会通过this隐式地将对象的地址传递给成员函数。通过this指针,成员函数可以访问和操作当前对象的成员变量和成员函数。

this指针只能在非静态成员函数中使用,因为静态成员函数没有this指针,它们不属于任何具体的对象

说说数组和指针的区别

参考回答

  1. 概念:

    (1)数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。

    (2)指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。 指针名指向了内存的首地址。

  2. 区别:

    (1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝

    (2)存储方式

    ​ 数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。

    ​ 指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。

    (3)求sizeof

    ​ 数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)

    ​ 在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。

    (4)初始化

    // 数组 int a[5] = { 0 }; char b[] = "Hello"; // 按字符串初始化,大小为6 char c[] = { 'H','e','l','l','o','\0' }; // 按字符初始化 int* arr = new int[10]; // 动态创建一维数组  
    // 指针 // 指向对象的指针 int* p = new int(0); delete p; // 指向数组的指针 int* p1 = new int[10]; delete[] p1; // 指向类的指针: string* p2 = new string; delete p2; // 指向指针的指针(二级指针) int** pp = &p; **pp = 10;

    (5)指针操作:

    ​ 数组名的指针操作

    int a[3][4];   int (*p)[4];  //该语句是定义一个数组指针,指向含4个元素的一维数组 p = a;    //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0] p++;          //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]               //所以数组指针也称指向一维数组的指针,亦称行指针。 //访问数组中第i行j列的一个元素,有几种操作方式: //*(p[i]+j)、*(*(p+i)+j)、(*(p+i))[j]、p[i][j]。其中,优先级:()>[]>*。 //这几种操作方式都是合法的。

    指针变量的数据操作:

    char *str = "hello,douya!";
    str[2] = 'a';
    *(str+2) = 'b';
    //这两种操作方式都是合法的。
    ​

说说什么是函数指针,如何定义函数指针,有什么使用场景

参考回答

  1. 概念:函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。

  2. 定义形式如下:

int func(int a);  
int (*f)(int a);  
f = &func;  
​
  1. 函数指针的应用场景回调(callback)。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。

答案解析

//以库函数qsort排序函数为例,它的原型如下:
void qsort(void *base,//void*类型,代表原始数组
           size_t nmemb, //第二个是size_t类型,代表数据数量
           size_t size, //第三个是size_t类型,代表单个数据占用空间大小
           int(*compar)(const void *,const void *)//第四个参数是函数指针
          );
//第四个参数告诉qsort,应该使用哪个函数来比较元素,即只要我们告诉qsort比较大小的规则,它就可以帮我们对任意数据类型的数组进行排序。在库函数qsort调用我们自定义的比较函数,这就是回调的应用。
​
//示例
int num[100];
int cmp_int(const void* _a , const void* _b){//参数格式固定
    int* a = (int*)_a;    //强制类型转换
    int* b = (int*)_b;
    return *a - *b;  
}
qsort(num,100,sizeof(num[0]),cmp_int); //回调
​

C++中函数指针和指针函数的区别。

在 C++ 中,函数指针(Function Pointer)和指针函数(Pointer to a Function)是两个不同的概念,它们具有不同的含义和用途。

1、函数指针(Function Pointer)函数指针是指向函数的指针,它可以用于存储和调用函数的地址。函数指针的声明形式为:return_type (*pointer_name)(parameter_types),其中 return_type 是函数的返回类型,parameter_types 是函数的参数类型列表。通过函数指针,可以在运行时动态地选择调用不同的函数。

// 声明一个函数指针
int (*funcPtr)(int, int);

// 初始化函数指针
funcPtr = add; // add 是一个函数名

// 使用函数指针调用函数
int result = funcPtr(3, 5); // 调用 add 函数

2、指针函数(Pointer to a Function)指针函数是一个返回指针的函数,它的返回值是一个指针,指向某种数据类型。指针函数的声明形式为:return_type* function_name(parameters),其中 return_type 是指针所指向的数据类型,parameters 是函数的参数列表。指针函数返回一个指针,该指针可以指向函数外部分配的内存或其他数据。

// 声明一个指针函数
int* createIntPointer();

// 定义指针函数
int* createIntPointer() {
    int* ptr = new int(10);
    return ptr;
}

// 使用指针函数
int* ptr = createIntPointer(); // 返回一个指向动态分配的整数的指针

总结区别:

  • 函数指针是指向函数的指针,可以调用函数。指针函数是返回指针的函数,返回一个指向某种数据类型的指针。
  • 函数指针声明时使用 (*ptr) 语法,指针函数声明时使用 return_type* 语法。
  • 函数指针存储函数地址,指针函数返回一个指针值
  • 函数指针用于动态调用不同函数,指针函数返回指针值供外部使用。

nullptr调用成员函数可以吗?为什么?

参考回答

能。

原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。

答案解析

//给出实例
class animal{
public:
    void sleep(){ cout << "animal sleep" << endl; }
    void breathe(){ cout << "animal breathe haha" << endl; }
};
class fish :public animal{
public:
    void breathe(){ cout << "fish bubble" << endl; }
};
int main(){
    animal *pAn=nullptr;
    pAn->breathe();   // 输出:animal breathe haha
    fish *pFish = nullptr;
    pFish->breathe(); // 输出:fish bubble
    return 0;
}  
​

原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。pAn->breathe();编译的时候,函数的地址就和指针pAn绑定了;调用breath(*this), this就等于pAn。由于函数中没有需要解引用this的地方,所以函数运行不会出错,但是若用到this,因为this=nullptr,运行出错。

说说运算符i++和++i的区别

参考回答

先看到实现代码:

#include <stdio.h>
int main(){
       int i = 2;
    int j = 2;
    j += i++; //先赋值后加
    printf("i= %d, j= %d\n",i, j); //i= 3, j= 4
    i = 2;
    j = 2;
    j += ++i; //先加后赋值
    printf("i= %d, j= %d",i, j); //i= 3, j= 5
}
​
  1. 赋值顺序不同:++ i 是先加后赋值;i ++ 是先赋值后加;++i和i++都是分两步完成的。

  2. 效率不同:后置++执行速度比前置的慢。

  3. i++ 不能作为左值,而++i 可以

    int i = 0;
    int* p1 = &(++i);//正确
    // int* p2 = &(i++);//错误
    ++i = 1;//正确
    // i++ = 1;//错误
    ​
  4. 两者都不是原子操作。

((void ()( ) )0)( )含义

这段代码看起来是在尝试使用函数指针来调用一个函数。让我们逐步解释这个表达式:

  1. ((void ()( ) )0):这是一个函数指针的调用。让我们从内部向外解释它:

    • void ()( ) 表示一个函数指针的类型,其中 ( ) 是函数参数列表(空参数),最后的 ( ) 表示函数返回类型为空(void)。
    • ((void ()( ) )0) 表示将整个函数指针类型强制转换为函数指针,并将其初始化为 0(空指针)。
  2. ((void ()( ) )0)( ):在这里,((void ()( ) )0) 表示一个函数指针,然后后面的 ( ) 是实际的函数调用,表示调用这个函数指针指向的函数。因为我们之前将函数指针初始化为 0(空指针),所以这实际上是在尝试调用一个空指针指向的函数。

在大多数情况下,这样的代码是错误的,因为试图通过一个空指针调用函数会导致未定义的行为。空指针不指向任何有效的函数代码,因此调用它会导致程序崩溃或异常。

简述C++有几种传值方式,之间的区别是什么?

参考回答

传参方式有这三种:值传递、引用传递、指针传递

  1. 值传递:形参即使在函数体内值发生变化,也不会影响实参的值;

  2. 引用传递:形参在函数体内值发生变化,会影响实参的值;

  3. 指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;

答案解析

值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。

代码示例

//代码示例
#include <iostream>
using namespace std;
​
void testfunc(int a, int *b, int &c){//形参a值发生了改变,但是没有影响实参i的值;但形参*b、c的值发生了改变,影响到了实参*j、k的值
    a += 1;
    (*b) += 1;
    c += 1;
    printf("a= %d, b= %d, c= %d\n",a,*b,c);//a= 2, b= 2, c= 2
}
int main(){
       int i = 1;
    int a = 1;
    int *j = &a;
    int k = 1;
    testfunc(i, j, k);
    printf("i= %d, j= %d, k= %d\n",i,*j,k);//i= 1, j= 2, k= 2
    return 0;
}

形参为int arr[] 和vector<int> vec, vector<int>& vec, vector<int>* vec

  • int arr[]传入函数的是数组的首地址,所以在进行交换等操作的时候要进行取地址&arr[i]。
  • function1(std::vector<std::vector > vec),传值
  • function2(std::vector<std::vector >& vec),传引用
  • function3(std::vector<std::vector >* vec),传指针

简述const(星号)和(星号)const的区别

参考回答

//const* 是常量指针,*const 是指针常量
int const *a;    //a指针所指向的内存里的值不变,即(*a)不变
int *const a;    //a指针所指向的内存地址不变,即a不变

常量取地址?

只有保存在内存中的变量或常量,才会有一个与之相关的内存编号(地址

1.代码中常量可以分两种:字面常量和限制常量。 字面常量没有名字,比如:cout<<8,这个8就是一个字面常量。是数据,占用一块内存,有内存地址,但是我们对它取不了址,因为它没有名字。取址符号是 &,可是要它在后面跟一个数据的名字。比如 &a 。写&8是不行的。就算行(硬要实现)也没有意义。因为一个没有名字(或者可以借助其它名字找到)的数据,意味着没有联系方式,意味着离开当前这个字面位置,你就再也无法合法地使用它。因此,对于代码中的数据,有名字代表就有联系方式。或者,直接就不叫名字,就当成是联系方式。

2.const int b = 10;可以对b取地址吗?可以。定义一个 const 量实际也是一个变量,const只限定它不能被修改,所有变量都可(在程序运行时)获取其地址。

3.enum类型中的枚举项(enumerator)只是enum类型声明的一部分,它不是定义出的变量,所以不能取值。

4.#define PI 3.14 出来的是宏,它是预处理的东西,编译器会很快将PI擦除,全部换回字面量3.14,预处理后的编译阶段已经不存在,所以也不可能获取宏的地址。

指针和引用的区别

相同点:

  1. 引用是被引用变量的别名,引用本身所占用的空间,存储的就是被引用变量的地址,没有自己的内存地址。这和指针变量所存储的内存相同。都是靠地址来实现的

  2. 它们作为形参时,都是双向传递,且都可以避免值复制从而减少函数调用时的数据传递开销。

不同点:

  1. 普通指针可以多次被赋值,即多次更改它所指向的对象。而引用只能在初始化时指定被引用的对象,其后就不能改了。

  2. 指针是一种底层机制,引用则是一种较高层的机制。

可以肯定的说,用引用能实现的功能,用指针都可以实现。 引用的本质还是一个指针常量

对于参数传递,使用引用比指针更加简洁、安全。但是很多情况还是不能代替指针的。

比如:中途改变所指对象、用空指针表达特定含义,但是没有空引用之说、函数指针,没有函数引用之说。

random会取0、1吗

C语言(0,1) java和python[0, 1);

序列化和反序列化?

​ 简单来说,序列化就是将对象实例的状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它依据流重构对象。这两个过程结合起来,能够轻松地存储和数据传输。比如,能够序列化一个对象,然后使用 HTTP 通过 Internet 在client和server之间传输该对象。

序列化:将对象变成字节流的形式传出去。

反序列化:从字节流恢复成原来的对象。

序列化目的:(1) 将对象存储于硬盘上 ,便于以后反序列化使用;(2)在网络上传送对象的字节序列

优点:网络传输方面的便捷性、灵活性,针对大工程节省时间,比如:

你有一个数据结构,里面存储的数据是经过非常多其他数据通过非常复杂的算法生成的,因为数据量非常大,算法又复杂,因此生成该数据结构所用数据的时间可能要非常久(或许几个小时,甚至几天),生成该数据结构后又要用作其他的计算,那么你在调试阶段,每次执行个程序,就光生成数据结构就要花上这么长的时间,无疑代价是非常大的。假设你确定生成数据结构的算法不会变或不常变,那么就能够通过序列化技术生成数据结构数据存储到磁盘上,下次又一次执行程序时仅仅须要从磁盘上读取该对象数据就可以,所花费时间也就读一个文件的时间,可想而知是多么的快,节省了我们的开发时间。

大数相乘,排列组合等为什么要取模

  • (10 ^ 9 + 7)1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复

  • int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。

  • int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。

  • 所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。

2.4 关键字

volatile关键字有什么作用

易变关键字,告诉编译器不要对这个变量进行优化,也就是说,每次都要去绝对地址上取值,而不能从寄存器上取值。

作用:

  1. 多线程应用中被多个任务共享变量。 多线程中修饰变量防止变量装入寄存器,使各线程在内存读取一致变量。

一个参数可以既是const又是volatile吗?

可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。

volatile [ˈvɑːlətl] adj.不稳定的;易挥发的;易变的;无定性的;无常性的;

说说 static关键字的作用

参考回答

  1. 全局静态变量: 全局变量用static修饰改变了作用域,没有改变生存周期。普通的全局变量是可以被其他的.c文件引用的,一旦被static修饰,就能被定义该全局变量的.c文件引用,使得该全局变量的作用范围减小
       作用:当一个全局变量不想被其他.c文件引用时,可以用static修饰,这样其他的文件就不能通过extern的方式去访问,这样主要是为了数据安全
       总结:改变其作用域,没有改变生存周期。

  2. 局部静态变量:局部变量就是在函数内定义的变量,生存周期是随着函数的结束而结束,不会保留上次的值。当用static修饰后,局部变量的生存周期就是当程序结束才会结束,会保留上一次的值。
        应用:在函数内,我们想保留某些变量上一次的值,就可以用static去修饰该变量。比如:想统计该函数被执行的次数时,就可以定义被static修饰的int型变量,每执行一次该变量就++。
        总结:用static修饰的局部变量,改变了生存周期,但是没有改变其作用域。改变其生存周期的原因是被static修饰的局部变量被存放在.bss段或者.data段,而普通的局部变量是存放在栈上的。

  3. 初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。未指定初值的静态生存期变量,会被赋予0值初始化,而对于动态生存期变量,不指定初值意味着初值不确定。 只不过全局静态变量和局部静态变量的作用域不一样;

  4. 定义静态函数:在函数返回类型前加上static关键字,函数即被定义为静态函数,改变了作用域。静态函数只能在本源文件中使用,普通的函数是可以通过头文件声名的方式被其他文件调用;有些函数并不想对外提供,只需要在本文件里。

  5. 在c++中,static关键字可以用于定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间。被类的所有对象共享,包括子对象。必须在类外初始化,不可以在构造函数内进行初始化。

  6. 在c++中,static关键字可以用于定义类中的静态成员函数:与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间。所有对象共享该函数,不含this指针,不可使用类中非静态成员。

答案解析

当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针(划重点,面试题常考)。既然它没有指向某一对象,也就无法对一个对象中的非静态成员进行访问。

什么情况下会使用静态变量

在程序设计中,当一个变量会被类的多个实例化对象所共享,以实现多个对象之间的通信,或用于记录已被创建的对象个数时,这种情况下使用静态变量。

用static修饰全局变量和函数,除了上面说的数据安全防止被误引用,还有一个作用是解决重名问题。当用static修饰了全局变量和函数后,其他文件里再定义同名的全局变量和函数也是可以的。一般来说,如果不是要对外提供的函数和全局变量,最好都用static修饰。

static修饰的函数,可以被其他文件调用吗?

函数:函数用static修饰,改变了作用域。普通的函数是可以通过头文件声名的方式被其他文件调用,被static修饰后就只能在本文件里被调用,这样是为了数据的安全。总结:改变了作用域,没有改变其生存周期。

这是和普通函数的区别,但是可以用过其他方式调用:

  1. 间接调用:在文件中定义一个对外提供的函数,该函数在内部调用static修饰的函数,这就实现了间接调用static修饰的函数。

  2. 直接调用:将static修饰的函数的函数指针传递出去,其他文件可以通过函数指针进行调用。

说说静态变量什么时候初始化?

参考回答

对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化

C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。

答案解析

  1. 作用域:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。

    静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。

    静态局部变量 :局部作用域,只被初始化一次,直到程序结束。

    类静态成员变量:类作用域。

  2. 所在空间:都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。

  3. 生命周期:静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。类静态成员变量在静态存储区,当超出类作用域时回收内存。

说说静态局部变量,全局变量,局部变量的特点,以及使用场景

参考回答

  1. 首先从作用域考虑:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。

    全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。

    静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。

    局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。

    静态局部变量 :局部作用域,只被初始化一次,直到程序结束。

  2. 从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。

  3. 生命周期: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。

  4. 使用场景:从它们各自特点就可以看出各自的应用场景,不再赘述。

说说new和malloc的区别,各自底层实现原理。

参考回答

  1. new是操作符,而malloc是函数。

  2. new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。

  3. malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。

  4. new可以被重载;malloc不行

  5. new分配内存更直接和安全。

  6. new发生错误抛出异常,malloc返回null

答案解析

malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:

  1. 创建一个新的对象

  2. 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)

  3. 执行构造函数中的代码(为这个新对象添加属性)

  4. 返回新对象

说说const和define的区别。

参考回答

const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:

  1. const生效于编译的阶段;define生效于预处理阶段。

  2. const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。

  3. const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。

  4. 定义域不同:#define宏不受定义域限制,而const常量只在定义域内有效。

说说内联函数和宏函数的区别

参考回答

区别:

  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。

  2. 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率

  3. 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

答案解析

//宏定义示例
#define MAX(a, b) ((a)>(b)?(a):(b))
MAX(a, "Hello"); //错误地比较int和字符串,没有参数类型检查
​
//内联函数示例
#include <stdio.h>
inline int add(int a, int b) {
    return (a + b);
}
int main(void) {
    int a;
    a = add(1, 2);
    printf("a+b=%d\n", a);
    return 0;
}
//以上a = add(1, 2);处在编译时将被展开为:a = (a + b);
​

1、使用时的一些注意事项:

  • 宏定义函数没有类型检查,还有就是括号的使用,宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性

  • inline函数一般用于比较小的,频繁调用的函数,这样可以减少函数调用带来的开销。只需要在函数返回类型前加上关键字inline,即可将函数指定为inline函数。

  • 同其它函数不同的是,最好将inline函数定义在头文件,而不仅仅是声明,因为编译器在处理inline函数时,需要在调用点内联展开该函数,所以仅需要函数声明是不够的。

2、内联函数使用的条件:

  • 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

  • (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

  • (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

  • 内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。

内联函数inline

对于一个频繁使用的短小函数,应使用inline内联函数,即编译器将inline内联函数内的代码替换到函数被调用的地方。

优点:(1)省去函数调用的时间,从而提高程序运行效率;(2)相比于宏函数,内联函数在代码展开时,编译器会进行语法安全检查或数据类型转换,使用更加安全;

缺点:(1)代码膨胀,产生更多的开销;(2)如果内联函数内代码块的执行时间比调用时间长得多,那么效率的提升并没有那么大;

内联声明只是建议,是否内联由编译器决定,所以实际并不可控。

说说内联函数和函数的区别,内联函数的作用。

参考回答

  1. 内联函数比普通函数多了关键字inline

  2. 内联函数避免了函数调用的开销;普通函数有调用的开销

  3. 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。

  4. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。

内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。

答案解析

在使用内联函数时,应注意如下几点:

  1. 在内联函数内不允许用循环语句和开关语句。 如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。

  2. 内联函数的定义必须出现在内联函数第一次被调用之前。

内联函数有什么缺点?

内联函数的缺点主要有以下几点:

  • 代码膨胀:内联函数会在每个调用它的地方进行代码替换,这可能导致代码膨胀。如果内联函数体非常大或者被频繁调用,会增加可执行文件的大小,可能导致缓存不命中,影响性能。

  • 编译时间增加:内联函数需要在每个调用点进行代码替换,这会增加编译时间。特别是当内联函数被广泛使用时,编译时间可能会显著增加。

  • 可读性降低:内联函数会将函数体嵌入到调用点,可能导致代码的可读性降低。函数体被分散在多个地方,可能会使代码难以理解和维护。

typedef 和define 区别

  1. 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏

  2. 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换不进行类型的检查

  3. 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。

    注意:typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号。

override和final

  • override:保证在派生类中声明的函数,与基类的虚函数有相同的签名;

  • final:阻止类的进一步派生 和 虚函数的进一步重写。

例如:加了override,明确表示派生类的这个虚函数是重写基类的,如果派生类与基类虚函数的签名不一致,编译器就会报错。

class Base {
public:
    virtual void Show(int x); // 虚函数
};
​
class Derived : public Base {
public:
    virtual void Show(int x) const override; // const 属性不一样,新的虚函数 
};
//会报错,因此,为了减少程序运行时的错误,重写的虚函数都建议加上 override。

例如:如果不希望某个类被继承,或不希望某个虚函数被重写,则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,再被继承或重写,编译器就会报错。

class Base {
public:
    virtual void Show(int x) final; // 虚函数
};
​
class Derived : public Base {
public:
    virtual void Show(int x) override; // 重写提示错误  
};
//因此,一旦一个虚函数被声明为final,则派生类不能再重写它。

2.5 崩溃

说说什么是野指针,怎么产生的,如何避免?

参考回答

  1. 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

  2. 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。

  3. 避免办法:

    (1)初始化置NULL

    (2)申请内存后判空

    (3)指针释放后置NULL

    (4)使用智能指针

答案解析

产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。如:

char *p = (char *)malloc(sizeof(char)*100);  
strcpy(p, "Douya");  
free(p);//p所指向的内存被释放,但是p所指的地址仍然不变  
...  
if (p != NULL){//没有起到防错作用  
    strcpy(p, "hello, Douya!");//出错  
}  
​

避免办法:

(1)初始化置NULL

(2)申请内存后判空

(3)指针释放后置NULL

int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间  
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);  
p = NULL; //释放后置空
​
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 
assert(p1 != NULL); //判空,防错设计
free(p1);  
p1 = NULL; //释放后置空
​
int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间  
assert(p2 != NULL); //判空,防错设计
delete []p2;  
p2 = nullptr; //释放后置空  
​

悬挂指针与野指针区别?

悬挂指针:悬挂指针是指一个指针仍然存在,但它所指向的内存已经被释放或者无效。在程序中,当你释放了某个内存块,并且该内存块的指针仍然存在,那么这个指针就成为悬挂指针。使用悬挂指针可能会导致未定义的行为,因为你在试图访问已经被释放的内存区域。

int* danglingPtr() {
    int x = 10;
    int* ptr = &x; // ptr指向了一个局部变量
    return ptr;
}

int main() {
    int* p = danglingPtr();
    // 此时p成为了悬挂指针,因为它指向了已经被释放的内存区域
    // 在访问*p时会导致未定义行为
    return 0;
}

野指针:野指针是指一个未初始化或者已被释放的指针,它指向一个未知的内存地址。使用野指针会导致访问未知或者无效的内存区域,这也可能会导致未定义的行为。野指针通常是因为没有正确初始化指针或者在释放内存后未将指针设为 null 或合法的内存地址。

避免野指针:

  1. 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。

  2. 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。

  3. 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

int* wildPtr() {
    int* ptr; // 未初始化的指针
    *ptr = 5; // 这里会导致未定义行为,因为ptr指向未知的内存地址
    return ptr;
}

int main() {
    int* p = wildPtr();
    // p成为了野指针,因为ptr未初始化,指向未知的内存地址
    return 0;
}

说说使用指针需要注意什么?

参考回答

  1. 定义指针时,先初始化为NULL。

  2. 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  3. 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  4. 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作

  5. 动态内存的申请与释放必须配对,防止内存泄漏

  6. 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”

答案解析

(1)初始化置NULL

(2)申请内存后判空

(3)指针释放后置NULL

int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间  
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);  
p = NULL; //释放后置空
​
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 
assert(p1 != NULL); //判空,防错设计
free(p1);  
p1 = NULL; //释放后置空
​
int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间  
assert(p2 != NULL); //判空,防错设计
delete []p2;  
p2 = nullptr; //释放后置空  
​

​​​​​​

猜你喜欢

转载自blog.csdn.net/shisniend/article/details/131908934