C++基础(待)

265 阅读20分钟

参考

  1. 公众号:程序员贺先生——C++八股

  2. (46条消息) C语言--结构体内存对齐规则_c语言结构体对齐原则_->小黑的博客-CSDN博客

  3. (46条消息) 为什么C++静态static函数不能访问非静态成员_Wzning0421的博客-CSDN博客

  4. 公众号:程序员贺先生——C++八股

  5. Go 与 C++ 的对比和比较 - 知乎 (zhihu.com)

预处理,编译,汇编,链接程序的区别

image.png ⼀段⾼级语⾔代码经过四个阶段的处理形成可执⾏的⽬标⼆进制代码。 预处理器→编译器→汇编器→链接器:最难理解的是编译与汇编的区别。 这⾥采⽤《深⼊理解计算机系统》的说法。

预处理阶段: 写好的⾼级语⾔的程序⽂本⽐如 hello.c,预处理器根据 #开头的命令,修改原始的程序,如#include<stdio.h> 将把系统中的头⽂件插⼊到程序⽂本中,通常是以 .i 结尾的⽂件。
包括宏定义、文件包含、条件编译、注释删除、添加行号

编译阶段: 编译器将 hello.i ⽂件翻译成⽂本⽂件 hello.s,这个是汇编语⾔程序。⾼级语⾔是源程序。所以注意概念之间的区别。汇编语⾔程序是干嘛的?每条语句都以标准的⽂本格式确切描述⼀条低级机器语⾔指令。不同的⾼级语⾔翻译的汇编语⾔相同。

  • 词法分析:按照用户描述好的词法规则将输入字符串分割
  • 语法分析:按照用户给定的语法规则,将词法分析产生的记号序列解析成语法树。
  • 语义分析:静态语义(编译时确定语义如类型匹配转换),动态语义(运行时确定语义如除数是0)

汇编阶段: 汇编器将 hello.s 翻译成机器语⾔指令。把这些指令打包成可重定位⽬标程序,即.o⽂件。hello.o是⼀个⼆进制⽂件,它的字节码是机器语⾔指令,不再是字符。前后两个阶段 都还有字符。

链接阶段: ⽐如 hello 程序调⽤ printf 程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于⼀个名叫 printf.o 的单独编译好的⽬标⽂件中,这个⽂件将以某种⽅式合并到 hello.o 中。连接器就负责这种合并。得到的是可执⾏⽬标⽂件。
连接器将目标文件和你用到的相关库文件一起链接形成main.exe。

动态编译与静态编译

静态编译,编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,连接到可执⾏⽂件中去,使可执⾏⽂件在运⾏时不需要依赖于动态链接库;

动态编译,可执⾏⽂件需要附带⼀个动态链接库,在执⾏时,需要调⽤其对应动态链接库的命令。
优点:缩⼩了执⾏⽂件本身的体积;加快了编译速度,节省了系统资源。
缺点: 哪怕是很简单的程序,只⽤到了链接库的⼀两条命令,也需要附带⼀个相对庞⼤的链接库;
⼆是如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不能运⾏。

动态链接和静态链接区别

静态连接库就是把 (lib) ⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件;
动态链接就是把调⽤的函数所在⽂件模块(DLL)和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码,因此需要相应DLL ⽂件的⽀持。 动态库就是在需要调⽤其中的函数时,根据函数映射表找到该函数然后调⼊堆栈执⾏。如果在当前⼯程中有多处对dll⽂件中同⼀个函数的调⽤,那么执⾏时,这个函数只会留下⼀份拷⻉。 但如果有多处对 lib ⽂件中同⼀个函数的调⽤,那么执⾏时该函数将在当前程序的执⾏空间⾥留下多份拷⻉,⽽且是⼀处调⽤就产⽣⼀份拷⻉。

区别点1:静态链接库与动态链接库都是共享代码的⽅式,如果采⽤静态链接库,则⽆论你愿不愿意,lib中的指令都全部被直接包含在最终⽣成的 EXE ⽂件中了。但是若使⽤ DLL,该 DLL 不必被包含在最终 EXE ⽂件中,EXE ⽂件执⾏时可以“动态”地引⽤和卸载这个与 EXE 独⽴的 DLL ⽂件。

区别点2:静态链接库和动态链接库的另外⼀个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,⽽在动态链接库中还可以再包含其他的动态或静态链接库。

C和C++区别(函数/类/struct/class)

  • C++ 有新增的语法和关键字:语法的区别有头⽂件的不同和命名空间的不同,关键字⽅⾯⽐如 C++ 与 C 动态管理内存的⽅式不同,C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,⽽且 C++ 中在指针的基础上增加了引⽤的概念,关键字例如 C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要求,还有 dynamic_cast 增加类型安全⽅⾯的内容。
  • 函数⽅⾯ C++ 中有重载和虚函数的概念:C++ ⽀持函数重载⽽ C 不⽀持,是因为 C++ 函数的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后⾯,例如,int func(int,double)经过名字修饰之后会变_func_int_double,⽽ C 中则会变成 _func,所以 C++ 中会⽀持不同参数调⽤不同函数。
  • C++ 还有虚函数概念,可以实现多态。
  • C 的 struct 和 C++ 的类也有很⼤不同:
    1. C++ 中的 struct 不仅可以有成员变量还可以成员函数, 2. 对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继承权限都是 public
    2. C++ 中除了 struct 还有 class 表示类,struct 和 class 还有⼀点不同在于 class 的默认成员访问权限和默认继承权限都是 private。
  • C++ 中增加了模板还᯿⽤代码,提供了更加强⼤的 STL 标准库。

1678537696378.png

C++和go区别:

Go语言在网络应用编程方面比较方便,对并发控制较好 Go 是专为现代多核处理器而设计的。Go 语言支持并发编程,这意味着它可以使用不同的线程同时运行多个处理过程,而不是同一时刻只运行一个任务。它还具有延迟垃圾回收功能,可以进行内存管理以防止内存泄漏。

  • 速度和可读性: C++ 编写代码更复杂,Go 代码更紧凑。他围绕着简单性和可扩展性而构建。C++ 的编译时间非常慢。而编译时间依赖于实际编码的内容。和 C++ 比起来 Go 的编译时间明显更快。
  • 数据结构:
    C++ 是众所周知且都熟悉的面向对象结构,而 Go 却是过程式的并发型编程语言。和 C++ 不同,Go 没有带构造器和析构器的类。
  • 原发并发: Go支持原生的并发,而C、CPP不支持,而且Go的并发方式还是目前最高效的一种并发

C++ 的四种强制转换

建议尽量避免强制类型转换,干扰了正常的类型检查。

  • static_cast:静态转换 **static_cast<type-id> (expression)** 。
    任何具有明确定义的类型转换,只要不包含底层const,都可以使用.

    • 因为没有动态类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类)不安全。 Cat* cat = (Cat*)anim;这样是不安全的,因为Cat所占大小比Animal更大,而申请空间只有Animal大小,指针转换后可能会导致越界。
      如果发生多态,Animal* anim = new Cat; Cat* cat = (Cat*)anim; 本身已经申请了cat空间,那么就不会导致越界问题。

    所以主要执⾏⾮多态的转换操作;

  • dynamic_cast:动态转换,专⻔⽤于派⽣类之间的转换 type-id 必须是类指针,类引⽤或 void*,对于下⾏转换是安全的。 当类型不⼀致时,转换过来的是空指针,⽽static_cast,当类型不⼀致时,转换过来的是错误意义的指针,可能造成⾮法访问等问题。

    • 基础类型不可转。非常严格,失去精度,或者被不安全都不可以转换。
    • 父转子类不可以转(不安全)。如果发生了多态,父转子会安全,则可转。
  • const_cast:常量转换,专⻔⽤于 const 属性的转换,唯⼀⼀个可以操作常量的转换符
    去除 const 性质,或增加 const 性质
    只能针对底层const。非指针和非引用变量不能从const->非const

  • reinterpret_cast:重新解释转换,高危操作
    从底层对数据进行重新解释,依赖具体的平台,可移植性差;可以将整形转换为指针,也可以 把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。

const修饰指针如何区分

1678415837763.png 理解这些声明的技巧在于,查看关键字const右边来确定什么被声明为常ᰁ ,如果该关键字的右边是类型,则值是常ᰁ;如果关键字的右边是指针变ᰁ,则指针本身是常量。

底层const和顶层const 以及指针

int *a[10]; // 数组,指向int类型的指针的数组 int (a)[10]; // 指针,指向有10个int类型的数组的指针 int (a)(int); // 函数指针,指向有一个参数并且返回类型均为int的函数 int a(int); // 函数, 定义一个int参数并且返回类型为int的函数a int (*a[10])(int); // 函数指针的数组,指向有一个参数并且返回类型为int的函数的数组

深拷贝和浅拷贝

当出现类的等号赋值时,会调⽤拷⻉函数,在未定义显示拷⻉构造函数的情况下, 系统会调⽤默认的拷⻉函数-即浅拷⻉,它能够完成成员的⼀⼀复制。当数据成员中没有指针时,浅拷⻉是可⾏的。

但当数据成员中有指针时,如果采⽤简单的浅拷⻉,则两类中的两个指针指向同⼀个地址,当对象快要结束时,会调⽤两次析构函数,⽽导致指野指针的问题。

所以,这时必需采⽤深拷⻉。深拷⻉与浅拷⻉之间的区别就在于深拷⻉会在堆内存中另外申请空间来存储数据,从⽽也就解决来野的问题。简⽽⾔之,当数据成员中有指针时,必需要⽤深拷⻉更加安全。

C++结构体和类区别:

  • 默认权限:struct默认公有,class类默认私有
  • 类型:struct是值类型,class是引用类型
  • 存储:struct是栈存储,class是堆存储

什么时候适合用结构体:
因为结构体是值类型存在栈上,存储速度快因此,数据量较小的纯数据类型适合用结构体。
一般只有简单的数据没有复杂的实现方法和继承关系的时候用结构体

结构体内存对⻬⽅式和为什么要进⾏内存对⻬?

边界对齐是计算机系统中的一种内存优化策略,它通过确保数据结构的起始地址符合特定的对齐要求来提高内存访问效率。在16位机器上,通常的边界对齐是以2字节(16位)为单位进行的,因为这样可以确保内存访问的效率最大化。

内存对齐是指将数据结构中的每个成员按照一定的规则进行排列,使得每个成员的起始地址相对于该结构的起始地址偏移量为该成员大小的整数倍。这样做的目的是为了让处理器在读取数据时更加高效,因为处理器可以一次性读取多个连续地址上的数据,如果数据不对齐,处理器就需要多次读取,降低了读取速度。

对齐原则: 1678535424967.png

内存对齐作用(使用场景):

  • 平台移植性好 有的 CPU 遇到未进⾏内存对⻬的处理直接拒绝处理,不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • CPU处理效率高 内存读取粒度:CPU把内存当成一块一块的,块的大小可以说是2、4、8、16字节大小。CPU在读取时是一块一块的。块的⼤⼩称为内存读取粒度。

    若CPU的读取粒度为4字节,
    那么对于一个int 类型,若是按照内存对齐来存储,处理器只需要访存一次就可以读取完4个字节到寄存器。
    若没有按照内存对其来读取,就需要访问内存两次才能读取出一个完整的int 类型变量. 具体过程为,第一次拿出 4个字节,丢弃掉第一个字节,第二次拿出4个字节,丢弃最后的三个字节,然后拼凑出一个完整的 int 类型的数据。

  • 减少内存碎片: 对齐可以保证结构体或类中的数据成员按照规则排列,避免因为数据成员的大小不一致而导致的内存碎片。

指针参数传递和引⽤参数传递

指针参数传递本质上是值传递,它所传递的是⼀个地址值。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从⽽形成了实参的⼀个副本(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进⾏的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会 变)。

引⽤参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

引⽤传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变ᰁ,但是任何对于引⽤参数的处理都会通过⼀个间接寻址的⽅式操作到主调函数中的相关变ᰁ。⽽对于指针传递的参数,如果改变被调函数中的指针地址,它将应⽤不到主调函数的相关变ᰁ。如果想通过指针参数传递来改变主调函数中的相关变ᰁ(地址),那就得使⽤指向指针的指针或者指针引⽤。

从编译的⻆度来讲,程序在编译时分别将指针和引⽤添加到符号表上,符号表中记录的是变量名及变量所对应地址。
指针变ᰁ在符号表上对应的地址值为指针变ᰁ的地址值,⽽引⽤在符号表上对应的地址值为引⽤对象的地址值(与实参名字不同,地址相同)。
符号表⽣成之后就不会再改,因此指针可以改变其指向的对象(指针变ᰁ中的值可以改),⽽引⽤对象则不能修改。

函数传递参数的几种方式

  • 值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
  • 指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
  • 引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯。

四种cast

C++ 是一种强类型的语言。 许多转换,特别是那些暗示对值有不同解释的转换,需要显式转换,在 C++ 中称为类型转换。 y = int (x); 这些类型转换的通用形式的功能足以满足基本数据类型的大多数需求。但是不加选择地应用于类和指向类的指针,但是这可能导致代码在运行时错误(编译可能没有问题),不能进行错误检查。为了控制类之间的这些类型的转换。我们有四个特定的转换运算符:dynamic_cast、reinterpret_cast、static_cast 和 const_cast。 格式: xx_cast< new_type >( 要转换的表达式 )

  1. const_cast:将 const 变量转为非 const。

  2. static_cast: 用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

  3. dynamic_cast:其目的是确保类型转换的结果指向目标指针类型的有效完整对象。 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
    向上转换:指的是子类向基类的转换 向下转换:指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。动态安全检查

  4. reinterpret_cast:可以做任何类型的转换 ,既不检查指向的内容,也不检查指针类型本身。容易出问题。运算结果是从一个指针到另一个指针的值的简单二进制副本。

pragma once作用

1、编译器预编译命令,常用于头文件 2、命令内容显而易见:仅编译一次 3、用途:常出现在头文件中。因为同一头文件会在许多源文件中多次引用。如果没有指定编译一次,则编译时出现重定义错误。

new 和delete区别

1、类型不同:new/delete是C++的操作符,而malloc/free是C中的函数。

2、做的事情不同:new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。

3、结果不同:new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。

C的结构体和C++结构体的区别

C的结构体只是把数据变量给包裹起来了,并不涉及算法。而C++是把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。

  1. 就函数来说:C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。所以C的结构体是没有构造函数、析构函数、和this指针的。
  2. 就访问权限来说:C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
  3. 就继承来说:C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。

宏定义

和函数的区别:

  • 阶段:宏在预处理阶段完成替换、相当于直接插入代码。函数调用在运行时需要跳转到具体调用函数。
  • 返回值:宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
  • 类型检查:宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。

和typedef区别:

和const区别:

  • 阶段:define是在编译的预处理阶段起效果、const是在编译运行时候起作用
  • 类型检查:define只做替换、不做类型检查和计算、也不求解,const常量有数据类型,编译器会进行安全检查

和内联函数的区别:

  • 阶段:define是在编译前替换,内联函数可以进行参数类型检查(编译时),且内联函数具有返回值

宏定义的缺点:
#define MIN(a, b) ((a) < (b) ? (a) : (b)), 使用这个宏来比较表达式 MIN(p+1, b),它会被展开为:((*p+1) < (b) ? (*p+1) : (b)),这可能导致意外的行为,尤其是当 *p 的表达式具有副作用时。例如,如果 *p 引发了某个函数调用,它将被展开两次,可能导致函数副作用发生两次。 eg:给p换成函数会导致函数调用两次;
解决:替换成内联函数、编译时替换

struct和class区别

  • 访问属性:struct默认是公有的,class则默认是私有的
  • 默认继承权限:class默认是private继承, 而struct默认是public继承

顶层const和底层const

顶层const:指的是const修饰的变量本身是一个常量, eg:int* const b1 = &a;

底层const:指的是const修饰的变量所指向的对象是一个常量,eg:const int* b2 = &a;

常量指针和指针常量

常量指针:指向常量的指针,含义是不可以通过指针修改其指向的值。可以指向非常量,可改指向。
const int *p=&a //常量指针
指针常量:指指针本身的值是常量、,含义是不可更改指向
int * const p=&a //指针常量

实现string类

/*
    内部肯定要维护一个char指针和长度
    构造函数:无参、拷贝构、与C风格字符串的相互转换,移动构造函数、
    运算符:赋值
*/
#include <iostream>
#include <cstring>

class MyString{
private:
    char *data;
    size_t length;
public:
    // 构造函数
    // 无参
    MyString(): data(nullptr),length(0){}
    // CString字符串 strcpy
    MyString(const char *str): length(strlen(str)){
        data = new char[length+1];
        strcpy(data,str);
    }
    // 拷贝,类似CString字符串,  成员函数中,可以访问同类的其他对象的私有成员
    MyString(const MyString &tmp): length(tmp.length){
        data = new char[length+1];
        strcpy(data, tmp.data); 
    }
    // 移动:
    MyString(MyString &tmp): length(tmp.length),data(tmp.data){
         tmp.length = 0;
         tmp.data = nullptr;
    }
    
    // 运算符:先判断再delete再new
    MyString& operator=(const MyString &tmp){
        if(this != &tmp){
            delete[] data;
            length = tmp.length;
            data = new int[length+1];
            strcpy(data, tmp.data);
        }
        return *this;
    }
    MyString& operator=(Mystring &&tmp){
        if(this != &tmp){
            delete[] data;
            data = tmp.data;
            tmp.data = nullptr;
            length = tmp.length;
            tmp.length = 0;
        }
        return *this;
    }
    
    // c_str 获取C风格字符串
    const char* c_str() const{
        return data;
    }
}

int main(){
    MyString s1("hello");
    MyString s2 = s1;
    MyStrign s3 = move(s1);
    
    
}

线程安全如何保证

  1. 互斥锁
  2. 原子操作

inline 和 define 区别

  1. 对内联函数的处理发生在编译期,对宏定义的处理发生在预处理;
  2. 内联函数能够提供类型检查,相比宏定义更加安全;
  3. 内联函数只是给编译器在编译时提供优化建议,使内容短小简单的处理不以发生函数调用的方式进行,减少函数调用的寻找函数入口地址,保存现场以及返回时发生的开销,当内联函数过于复杂时,编译器会选择按照一般函数的方式编译,不会在调用的位置展开替换,而宏定义一定会展开替换