嵌入式八股文

C/C++基础

C和C++有什么区别

  • C++是⾯向对象的语⾔,⽽C是⾯向过程的语⾔;
  • C++引⼊ new/delete 运算符,取代了C中的 malloc/free 库函数;
  • C++引⼊引⽤的概念,⽽C中没有;
  • C++引⼊类的概念,⽽C中没有;
  • C++引⼊函数重载的特性,⽽C中没有。

基本数据类型

在STM32编程中,基本数据类型与C语言的数据类型相似。下面是一些常用的基本数据类型:

整型:

  • short:有符号短整数类型,通常为16位。unsigned short:无符号短整数类型,通常为16位。
  • int : 有符号整数类型,通常为32位。 unsigned int:无符号整数类型,也通常为32位。
  • long:有符号长整数类型,通常为32位。(在64位系统上可能为8字节)。

unsigned long:无符号长整数类型,通常为32位。

字符型:

  • char:有符号字符类型,通常为8位。unsigned char:无符号字符类型,通常为8位。

浮点型:

  • float:单精度浮点数类型,通常为32位。
  • double:双精度浮点数类型,通常为64位。

布尔型:

  • bool:布尔数据类型,C语言中占8位。C99标准引入,#include<stdbool.h>

关键字

1. Volatile有什么作用

  • 状态寄存器一类的并行设备硬件寄存器常用Volatile会告诉系统不要优化对变量的读取或写入顺序
  • 一个中断服务程序中修改的供其他程序检测的变量。
  • 多线程应用中几个任务共享的变量。 Volatile会让系统直接从地址取值,而非从缓冲寄存器中取值,防止ABA问题

Volatile会让系统直接从地址取值,而非从缓冲寄存器中取值,防止ABA问题

cpp 复制代码
  XBYTE[2]=0x55;
  XBYTE[2]=0x56;
  XBYTE[2]=0x57;
  XBYTE[2]=0x58;

对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入 volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)。

2. Static关键字

Static有什么作用

1.定义变量

静态全局变量:表示变量的作用域仅限于本文件,仅初始化一次,如果是在头文件中定义了静态变量,调用该变量的每个.c文件都会有一个独立的静态变量。

静态局部变量:生命周期不会随函数结束而结束,但是函数外面不能使用该变量,仅程序启动时被初始化一次

注意extern 和 static 的含义是互相矛盾的,不会同时用
2. 定义函数

在函数返回类型前加上 static 关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用 ;也就是说在其他源文件中可以定义和自己名字一样的函数

  1. 定义类中的静态成员变量

在类中定义静态成员变量时,不能在类里面初始化,不占用类内存空间,必须定义才能使用

结构体中不支持定义静态成员变量

  1. 定义类中的静态成员函数

静态成员函数是类的一部分,而不是对象的一部分,对象的静态成员数据共享对象的静态存储空间。静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。类的静态成员函数不能对类中的非静态成员进行访问,即不能再静态函数里面使用this指针。

在C语言中,为什么static变量只初始化一次?

对于所有的对象(不仅仅是静态对象),初始化都只有一次 ,而由于静态变量具有"记忆"功能,初始化后,一直都没有被销毁,都会保存在内存区域中 ,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序"同生死、共存亡",所以它只需初始化一次。而auto变量,即自动变量,由于它存放在栈区,一旦函数调用结束,就会立刻被销毁。

3. 结构体 Struct 和联合体 Union

C语言中 struct 与 union的区别是什么?

联合体中所有成员共用一块地址空间,联合体的大小等于其最大成员的大小。对联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了。

而结构体不同成员会存放在不同的地址,结构体的按大小等于所有所有成员大小之和。

举例,下面代码执行结果是多少?

cpp 复制代码
1	typedef union {double i; int k[5]; char c;}DATE;
2	typedef struct data{int cat; DATE cow;double dog;}too;
3	DATE max;
4	printf ("%d", sizeof(too)+sizeof(max));

假设机器为32位,int占4个字节,double占8个字节,char占1个字节,而DATE是一个联合型变量,最大变量类型是Int[5],所以占用20个字节,又因为联合型变量DATE中double占了8个字节,union要求占用空间能容纳最大成员空间,同时为成员变量基础类型的整数倍,因此union类型变量DATE的空间要求8字节对齐,为24字节。

而结构体变量data的对齐规则要求每个成员的起始地址能被该成员变量类型的对齐数整除,示例中

int从0x00开始,占用0x00~0x03这4个字节,

DATA cow如果从0x04开始,占用0x04~0x1B,0x04%24=4,不为0,不满足对齐规则,因此需要以0x1C作为起始地址,占用0x1C(28)~0x33(51),

double为8个字节,如果以0x34开始,52%8!=0,需要以0x38开始,占用0x38(56)~0x3F(63)。共64个字节。

且结构体的大小要求为成员变量基础数据类型的整数倍

本例结构体空间计算

起始地址0~3 对齐4

起始地址28~51 满足首地址

起始地址56~63~,满足首地址

最终内存空间64字节是结构体中成员变量基础数据类型的整数倍

#pragma pack(1) 修改字节对齐量为1

在使用结构体时,

#pragram(1) //禁止字节对齐 (按1字节对齐)

Struct card{

int a;

char b[20];

double c;}

#pragram() //开启指针对齐

默认会使用字节对齐,因此使用指针*p利用偏移时很有可能会出现问题

因此如果要结合指针使用通常使用#pragma(1)禁止字节对齐

Struct结构体在C和C++中有什么区别
  1. C语言中结构体不允许有函数,C++可以

  2. C语言中结构体不允许继承,C++可以

  3. C语言中结构体的使用必须要用别名或者使用struct,不能直接使用例如:

Struct student

{

Int age;

Int num;

Int sex;

}

Typedef struct student student; //必须得有别名才能使用

或者在使用的时候加上struct,例如:

struct student student;

  1. 访问权限不同。struct在C中默认是共有public,不可以修改权限,在C++中权限可以修改。

  2. 初始化

在C中不可以初始化数据成员,C++可以初始化。

  1. C++中空结构体大小为1,C为0
使用Union判断大小端问题

大端字节序:高字节放低位地址,低字节放高地址

小端字节序:低字节存放在低位,高字节存放在高位

使用联合体判断大小端问题:

原理就是union可以一块地址存放不同类型的数据。

union里面放一个short=0x0102 放一个char[2],如果是大端char[0]会占高位,否则占低位

cpp 复制代码
union my
{
    short t;
    char b[2];
};

typedef union my MY;

int main()
{
    MY test;
    test.t=0X0102; //   小端字节序 0000 0001 0000 0010  大端字节序0000 0010 0000 0001 
if(test.b[0]==0x01 && test.b[1]==0x02)
    {
    printf("这是大端字节序\n");
    printf("%x\n",test.b[0]);
    printf("%x\n",test.b[1]);
    }
if(test.b[0]==0x02 && test.b[1]==0x01)
    {
    printf("这是小端字节序\n");
    printf("%x\n",test.b[0]);
    printf("%x\n",test.b[1]);
    }
return 0;
}
大小端转换如何实现

利用按位与、按位或和移位操作

cpp 复制代码
//32 位大小端交换
int swap(int value)
{
    value=((value & 0x000000ff)<<24)|
    ((value & 0x0000ff00)<<8)|
    ((value & 0x00ff0000)>>8)|
    ((value & 0xff000000)>>24);
    return value;
}

4. Const常量

Const有什么作用?
  1. 定义变量(局部变量或全局变量)为常量,例如:
cpp 复制代码
1    const int N=100;//定义一个常量N
2    N=50; //错误,常量的值不能被修改
3    const int n; //错误,常量在定义的时候必须初始化
  1. 修饰函数的参数,表示在函数体内不能修改这个参数的值。

  2. 修饰函数的返回值。

a. 如果给用 const修饰返回值的类型为指针 ,那么函数返回值(即指针)的内容是不能被修 改的,而且这个返回值只能赋给被 const修饰的指针。例如:

cpp 复制代码
1    const char GetString() //定义一个函数
2    char *str= GetString() //错误,因为str没有被 const修饰
3    const char *str=GetString() //正确

b. 如果用 const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函 数调用结束后这个临时变量生命周期也就结束了,因此把这些返回值修饰为const没有意义

  1. 节省空间,避免不必要的内存分配
cpp 复制代码
1	#define PI 3.14159//该宏用来定义常量
2	const doulbe Pi=3.14159//此时并未将P放入只读存储器中
3	double  i=Pi//此时为Pi分配内存,以后不再分配
4	double  I=PI//编译期间进行宏替换,分配内存
5	double  j=Pi//没有内存分配再次进行宏替换,又一次分配内存
什么情况下使用const关键字?
  1. 修饰一般常量。一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如:
cpp 复制代码
1 int const x=2;const int x=2
  1. 修饰常数组。定义或说明一个常数组可以采用如下格式:
cpp 复制代码
1 int const a[8]={1,2,3,4,5,6,7,8}
2 const int a[8]={1,2,3,4,5,6,7,8}
  1. 修饰常对象。常对象是指对象常量,定义格式如下:
cpp 复制代码
1	class A:
2	const A a:
3	A const a:
  1. 修饰常指针
cpp 复制代码
1	const  int*p;  //常量指针,指向常量的指针。即p指向的内存可以变,p指向的数值内容不可变
2	int const*p; //同上
3	int*const p;//指针常量,本质是一个常量,而用指针修饰它。 即p指向的内存不可以变,但是p内存位置的数值可以变
4	const int* const p;//指向常量的常量指针。即p指向的内存和数值都不可变
  1. 修饰常引用。被const修饰的引用变量为常引用,一旦被初始化就不能再指向其他对象了。

  2. 修饰函数的常参数。const修饰符也可以修饰函数的传递参数,格式如下:

cpp 复制代码
1    void Fun(const int Var)

告诉编译器Var在函数体中不能被改变,从而防止了使用者一些无意的或错误的修改。

  1. 修饰函数的返回值。 const修饰符也可以修饰函数的返回值,表明该返回值不可被改变

,格式如下:

cpp 复制代码
1	const int FunI();
2	const MyClass Fun2();
  1. 在另一连接文件中引用 const常量。使用方式有
cpp 复制代码
1	extern const int 1:
2	extern const int j=10;

5. Enum枚举

枚举的成员默认情况下都是整数类型,通常是int类型

cpp 复制代码
enum Color { 
    RED,  // 默认值为0 
    GREEN,  // 默认值为1 
    BLUE, // 默认值为2 
    BLACK = -1, // 显式赋值为-1
    WHITE = 10,// 显式赋值为10 
};

6. Typedef

#define 和 typedef的区别

#define 是C语言中定义的语法,用于宏定义,是预处理命令 ,在预处理时进行简单而机械的字符串替换,不作正确性检查

typedef是关键字,在编译时处理,有类型检查功能,用于给一个已经存在的类型一个别名。

注意#define 和 typedef定义指针的区别
cpp 复制代码
#define myptr int*p
myptr a,b;    //a 是int* a, b 是int b,因宏定义是简单的字符替换
typedef int* myptr;
myptr a,b;//a 是int *a, b是int* b

补充:int *p,q表示P是指针变量,q是int变量

typedef int (*function)(); //定义一个函数指针

7. Extern链接

Extern 的作用是什么

Extern用来声明外部变量。主要作用是告诉编译器我在其他文件定义了变量a,并且分配了空间,不再为该变量申请空间。

Extern "C" 的作用是什么

加上extern "C"后,会指示编译器这部分代码按C语言的进行编译 ,而不是C++的。

8. Register变量

表示该变量存放在cpu寄存器里面,该值不能取地址操作,并且必须是整数不能是浮点数。

9. Auto

一般情况下没有特别说明的局部变量都是默认为auto类型,存储在栈中

左值和右值是什么?

左值是指可以出现在等号左边的变量,左值的特点就是可写(可寻址)

右值是指只能出现在等号右边的变量,右值的特点就是可读。

说说右值和右值引用吧

引用 int &是C++的概念,C语言只有有解引用*和取地址&的说法

什么是临时量(术语为右值)

临时量可以是基本数据类型、对象、字符串等各种类型的临时对象。常用于计算表达式的值,表达式结束后即销毁。临时量通常是匿名的,没有直接的标识符与之相关联。

在C++98中,临时量只能作为Const &(常量引用)类型传递给函数,目的是为了确保对临时量的访问是只读的,参数其实就是复制了一份临时量去栈中执行,但由于临时量会随函数销毁,如果把临时量返回给引用类型,临时量销毁后引用类型的指向就不可预测了。不安全。

cpp 复制代码
int& foo() {
    int x = 42;
    return x; // 尝试返回对临时量的非 const 引用
}   //x销毁后,返回的引用将变得不可预测

为了解决C++98中右值,也就是临时量,只能绑定到Const常量来当函数参数的问题,C++引入了右值引用的概念,可以实现移动语义完美转发,提高了代码的灵活性。

**右值引用** 是一种引用类型,用 `&&` 表示,它可以绑定到右值。
移动语义指的是将资源所有权从一个对象转移到另一个对象,而不是进行昂贵的深层复制

cpp 复制代码
//MyData对象
class MyData {
private:
    std::vector<int> data;  //data成员

public:
    // 拷贝构造函数
    MyData(std::vector<int> v) : data(std::move(v)) {
        std::cout << "Data object constructed" << std::endl;
    }

    // 移动构造函数
    MyData(MyData&& other) noexcept : data(std::move(other.data)) {
        std::cout << "Data object moved" << std::endl;
    }
};

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // 使用移动语义来构造 MyData 对象
    //这是在调用移动构造函数的同时创建一个名为 `obj` 的 `MyData` 对象
    //std::move明确地将一个右值转变为右值引用,因此调用的是移动构造函数
    MyData obj(std::move(vec));//由于vec是一个已经被命名的对象,因此会作为左值传入

    return 0;
}

/*
std::move()是一个 C++ 标准库中的函数模板,位于 `<utility>` 头文件中。它用于将传入的参数转换为右值引用,表示我们愿意放弃对该参数的所有权,从而允许在移动操作中将其内容转移给其他对象。
*/

谈到右值引用时不可避免要谈到移动语义,以及右值引用和普通引用的区别

左值引用使用 '&' 符号声明,例如 int& ref = x; 引用是别名,与引用的对象指向相同的地址,且具有相同的地址。

右值引用使用 '&&' 符号声明,例如 int&& rref = 5;。

右值引用一般用来做移动语义和完美转发

左值引用就是普通引用,普通引用定义了不能修改,属于Const类型的常量

new/delete与malloc/free的区别是什么?

  1. new、delete 是运算符,而 malloc 和 free 是标准库函数。
  2. new/delete 在为对象申请分配内存空间时,可以自动调用构造函数/析构函数来完成对象的初始化/销毁。malloc、free 只能申请/释放内存空间,不能够自动调用构造函数/析构函数。
  3. malloc 上申请时需要指定申请的内存大小,返回值需要强制类型转换。

内部数据对象是指在当前编译单元(通常是一个源文件)中定义的数据对象,可直接访问和操作。

非内部数据对象是指在其他编译单元中定义的数据对象,通常通过指针进行访问。

C语言中"#"和"##"的用法

1. (#)字符串化操作符

作用:将宏定义中的传入参数转换成用一对双引号括起来的参数字符串。其只能用于有传入参数的宏定义中,且必须置于有宏定义的参数名前。如:

cpp 复制代码
#define example( instr ) printf("the input string is:\t%s\n", #instr)
#define example1( instr ) #instr    //当使用该宏定义时:

example( abc ); // 在编译时将会展开成:printf("the input string is:\t%s\n, "abc")
string str = example1( abc ); //将会展开成 string str ="abc"

2.(##)符号连接操作符

作用:将宏定义的多个形参转换成一个实际参数名。如:

cpp 复制代码
#define exampleNum( n ) num##n

使用:

cpp 复制代码
int num9 = 9;
int num = exampleNum( 9 ); //将会扩展成 int num = num9

注意:

a. 当用##连接形参时,##前后的空格可有可无

b. 连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义

c. 如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开

cpp 复制代码
#include <stdio.h>
#include <string.h>

#define STRCPY(a, b)	strcpy(a ## _p, #b)

int main()
{
char var1_p[20];
char var2_p[30];
STRCPY(var1, var2);           //strcpy(var1 ## _p, #var2)  strcpy(var1_p, "var2")  
STRCPY(var2, var1);           //strcpy(var2 ## _p, #var1)  strcpy(var2_p, "var1")  
printf("var1 = %s\n", var1_p);
printf("var2 = %s\n", var2_p);

//例如STRCPY(STRCPY(var1,var2),var2);
//这里是否会展开为:  strcpy(strcpy(var1_p,"var2")_p,"var2")?答案是否定的:
//展开结果将是:	strcpy(STRCPY(var1,var2)_p,"var2")
//## 阻止了参数的宏展开!如果宏定义里没有用到 # 和 ##, 宏将会完全展开
// 把注释打开的话,会报错:implicit declaration of function 'STRCPY'
return 0;
}

结果:
	var1 = var2
	var2 = var1

sizeof关键字和strlen函数

strlen("\0")=? sizeof("\0")=?

strlen("\0") = 0,sizeof("\0") = 2

strlen函数返回字符串的长度(在C/C++中,字符串是以"\0"作为结束符的)

sizeof关键字 以字节的形式给出操作数的存储大小

sizeof 和 strlen有什么区别?

strlen 与 sizeof 的差别表现在以下 5 个方面

  1. sizeof 是关键字,而strlen是函数
  2. sizeof 运算符的结果类型是 size_t,它在头文件中 typedef 为 unsigned int类型
  3. sizeof 可以用类型作为参数,strlen 只能以 char* 作为参数,而且必须是以"\0"结尾的。sizeof 还可以以函数作参数,如 int g(),则 sizeof( g() ) 的值等于 sizeof(int)
  4. 大部分编译程序的 sizeof 都是在编译 的时候计算的,可以通过 sizeof(x) 来定义数组维数。而strlen则是在运行期计算的
  5. 当数组作为参数传给函数时,传递的是指针而不是数组,即传递的是数组的首地址
  6. sizeof可以计算任何类型数组所占的字节数,而strlen只用于字符数组

int a[9]; sizeof(a)/sizeof(a[0])即得元素个数

字符串函数

字符串长度 int strlen(const char* s);

//求字符串长度,\0结束

cpp 复制代码
int m_strlen(const char* s)
{
    int size=0;
    if(s == NULL)
    return 0;
    while(*s++ != '\0')
    {
        size++;
    }
    return size;
}
字符串拷贝 char *strcpy(char *dest, const char *src);

//把一个字符串复制到另一个

cpp 复制代码
char * m_strcpy(char * dest,const char * src)
{
    char ret=dest;
    while(*src != '\0')
    {
    *dest++= *src;
    src++;
    }
return ret;
}
字符串比较 int strcmp(const char *s1, const char *s2);

//碰到第一个不一样的就返回ASCII码的差值

cpp 复制代码
int m_strcmp(const char* s1,const char *s2)
{
    t(s1 != NULL && s2 != NULL);
    while(*s1 && *s2 && (*s1 == *s2))
    {
        s1++;
        s2++;
    }
    return (*s1-*s2);
}
字符串拼接 char *strcat(char *dest, const char *src);
cpp 复制代码
char * m_strcat(char *dest, const char *src)
{
    char * m_dest= dest;
    while( *dest != '\0')
    {
        dest++;
    }
        while(*src != '\0')
    {
        *dest++=*src++;
    }
    return m_dest;
}
字符复制 void *memset(void *s, int c, size_t n);

//复制字符 c(一个无符号字符)到参数 str所指向的字符串的前 n个字符

memset常用来清空缓冲区memset(buffer, 0, 1024);这里的参数0是ascii码'\0'

cpp 复制代码
void *m_memset(void *s, int c ,size_t n)
{
    void *ret= s;
    while(n--)
    {
        *(char*)s++=(char)c;//这里注意类型强转
    }
    return ret;
}
从字符串复制前N个字节到目标字符串 void *memcpy(void *dest, const void *src, size_t n);
cpp 复制代码
void *memcpy(void *dest, const void *src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;
    for (size_t i = 0; i < n; i++) {
        d[i] = s[i];
    }
    return dest;
}
字符串转整数 int atoi(const char *nptr);
cpp 复制代码
int myAtoi(char * s){
    if(s== NULL)
    {
        return -1;
    }
    //移除空格
    while(*s == ' ')
    {
        s++;
    }
    //确定正负
    int sign=(*s=='-')?-1:1;
    if(*s == '-' || *s == '+')
    {
        s++;
    }
    //确定是数字而不是字符
    double num=0;
    while(*s >='0' && *s <='9')
    {
        num=num*10+*s - '0';
        s++;
    }
        return (num*sign);
}

求指针大小

在32位机器下,对于sizeof(指针变量都是) 4个字节,比如

cpp 复制代码
Int *a;

Sizeof(a);  //结果为4

求引用大小

cpp 复制代码
Sizeof(char &)  //1  引用大小和数据类型有关

不使用 sizeof,如何求数据类型占用的字节数?

cpp 复制代码
1	#include <stdio.h>
2	#define MySizeof(Value) (char *)(&value+1)-(char*)&value
3	int main()
4	{
5	    int i ;
6	    double f;
7	    double *q;
8	    printf("%d\r\n",MySizeof(i));
9	    printf("%d\r\n",MySizeof(f));
10	    printf("%d\r\n",MySizeof(a));
11	    printf("%d\r\n",MySizeof(q));
12	    return 0;
13  } //输出为:4 8 32 4

上例中, (char*) & Value 返回 Value的地址的第一个字节,(char*)(& Value+1) 返回value的地址的下一个地址的第一个字节,所以它们之差为它所占的字节数。

C语言编译过程中,volatile 关键字和 extern 关键字分别在哪个阶段起作用

volatile 作用于预处理阶段,因为代码优化是在编译阶段,extern 作用于链接阶段,用来将符号引用链接到真实的数据地址。

++a 和 a++有什么区别

a++是先把 a 赋值到临时空间后再+1;++a直接在地址上对 a +1,不需要开辟临时空间。

gets和scanf函数的区别(空格,输入类型,返回值)

  1. gets函数可以接收空格,scanf遇到空格就会结束

  2. gets函数仅用于读入字符串;scanf可以读入任意C语言基础类型变量

  3. gets的返回值为 char* 类型,当读入成功时会返回输入字符串指针地址,出错返回 NULL

scanf的返回值为 int 类型,返回成功赋值的变量个数,当遇到文件结尾标识时返回 EOF宏(负值)

gets和scanf都会在末尾添加\0

cpp 复制代码
//scanf输入数组
#include <stdio.h>

#define MAX_SIZE 100

int main() {
    int arr[MAX_SIZE];
    int n, i;
    
//要求先让用户输入数组大小
    printf("Enter the number of elements in the array: ");
    scanf("%d", &n);

    printf("Enter %d integers:\n", n);
    for (i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }

    printf("Elements in the array are: ");
    for (i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

printf()函数的返回值

printf() 的返回值是元素的字符个数+\n

cpp 复制代码
printf("%d",printf("%d",241));//打印出来的内容为输出的字符个数+\n
//printf的返回值为输出的数字的字符个数

printf碰到/r或/n停止打印

scanf("%d",&xx)注意事项

!!!多个scanf连着用需要用getchar()来清除换行符(一般不用)!!!

`getchar()` 函数可以清除换行符(`\n`)是因为它会读取输入缓冲区中的下一个字符,包括换行符。当你在使用 `scanf` 函数后按下回车键时,回车键会被输入缓冲区中的换行符`\n`表示。而当你调用 `getchar()` 函数时,它会读取输入缓冲区中的下一个字符,即换行符`\n`,并将其从输入缓冲区中移除。

当我 输入控制格式 不加回车换行(\n),输入一个数字后,回车就会结束输入,并跳转下一语句的执行。

cpp 复制代码
scanf("%d",&xx)

\n属于空白字符, 而输入控制格式中的空白符(空格(space)、制表符(tab)和新行符(newline))会使 scanf() 在输入流中(自己在键盘输入的)跳过一个或多个空白行,直到发现非空白字符为止才可以再次用回车键结束输入。

cpp 复制代码
scanf("%d\n",&xx)

所以在输入控制格式中没有\n的时候输入数字,回车键就可以结束输入跳转到下一语句。

当输入控制格式包含\n时候,输入数字之后回车,程序读取缓存区,并把数字匹配给%d,缓存区还有个\n,可是由于输入控制格式中的空白字符\n, scanf() 会跳过缓存区剩下的\n以及之后回车键输入的空白行,直到发现非空白字符为止才可以再次用回车键结束输入。

C语言随机数

cpp 复制代码
#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> int main() { 
    // 设置种子 通常情况下,可以rand(0)或rand(NULL)来使用当前时间作为种子
    srand(time(0)); 
    // 生成随机数 
    int randomNumber = rand(); 
    printf("Random number: %d\n", randomNumber); 
    return 0; 
}

如果你想生成一定范围内的随机数,可以使用取余运算符来限制范围。例如,要生成 1 到 100 之间的随机数,可以使用 `rand() % 100 + 1`

cpp 复制代码
int randomNumberInRange = rand() % 100 + 1; // 生成 1 到 100 之间的随机数

内存

c语言中内存分配的方式有几种?

  1. 静态存储区分配

内存分配在程序编译之前完成,且整个程序运行期间都存在。如全局变量,静态变量等。

  1. 栈上分配

在函数执行时,函数内局部变量的存储空间在栈上创建,函数执行结束时这些存储单元自动释放。

  1. 堆上分配

new/malloc 申请的内存都在堆上

堆与栈有什么区别

  1. 空间申请方式

栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放

  1. 空间申请大小的限制

栈是向低地址扩展的数据结构,是一块连续的内存区域。栈的大小是提前设置好的,如果溢出就会提示overflow。堆的申请是向高地址扩张的,是不连续的内存区域,这是由于系统使用链表存储空闲的内存地址。

  1. 空间申请效率

栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即
释放;堆则是存放在二级缓存中,速度要慢些

栈在C语言中有什么作用?

  1. 函数调用中和函数调用相关的函数返回地址,函数参数,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。
  2. 操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石

栈的空间最大值是多少?

Windos平台栈的最大空间是2M,Linux是8M 使用ulimit -s看linux线程栈的大小限制

C语言函数参数压栈顺序是怎样的?

C语言采用自右向左的压栈方式,主要是为了支持可变长参数。

可变长参数的典型例子是printf(const char* format,...),说白了,从右到左,已知参数必须在栈顶,C语言的可变长参数用占位符的形式确定了可变长参数的类型和个数,如果不在栈顶,则可变长参数的类型和个数是未知的,为寻址带来困难。

C++的内存管理是怎样的

在C++中,虚拟内存分为代码段数据段BSS段堆区文件映射区 以及栈区这六个部分。

代码段:包括只读存储区和文本区,只读存储区存储字符串常量,文本存储区存储程序的机器代码

数据段:存储程序中已初始化的全局变量和静态变量

BSS段:存储未初始化的全局变量和静态变量

堆区: 调用new/malloc函数时在堆区动态分配内存,需要调用delete/free来手动释放申请的内存

映射区:存储动态链接库以及调用mmap函数进行的文件映射

:使用栈空间存储函数的入口地址、参数、变量、返回值

在1G内存的计算机中能否malloc(1.2G)?为什么?

是有可能在1G的物理空间机器上malloc(1.2G)的内存的,因为malloc是向程序的虚拟空间申请一块虚拟地址空间,最终实际分配的物理内存是由操作系统决定的。

malloc成功返回申请的空间首地址,失败返回null

strcat、strncat、strcmp、strcpy哪些函数会导致内存溢出?如何改进?

`strcat` 用于将一个字符串追加到另一个字符串的末尾

char *strcat(char *destination, const char *source);

`strncat` 用于将一个字符串的一部分追加到另一个字符串的末尾

char *strncat(char *destination, const char *source, size_t num);

`strcmp`函数用于比较两个字符串,并根据它们的字典顺序返回一个整数值。

int strcmp(const char *str1, const char *str2); //1参数>2参数返回 1;

如果一个字符串是另一个字符串的子集,长度不同,则会比较 \0 和 字符

当使用字符串字面值初始化字符数组时,编译器会自动在字符串的末尾添加一个空字符 `'\0'`,表示字符串的结束

`strcpy`函数会将源字符串的内容复制到目标字符串中,并在目标字符串的末尾添加一个空字符('\0'),以表示字符串的结束。

char *strcpy(char *destination, const char *source);

strcat、strncat、strcpy这仨种函数严格来讲都有内存溢出的风险。

改进就是做好长度限制避免空间不足导致溢出。

malloc、calloc、realloc内存申请函数

申请堆内存
void *malloc(size_t size); //申请 size_t 个字节内存
void free(void *ptr); //释放内存,但是指针还是可以用
void *calloc(size_t nmemb, size_t size); //申请 nmemb 块 内存,每块 size_t 个字节
void *realloc(void *ptr, size_t size); //申请内存,重新申请 size_t 字节内存
void *reallocarray(void *ptr, size_t nmemb, size_t size); //重新申请 nuemb个size_t 字节内存

说明:

  1. malloc(size)

不会对内存初始化。

如果size为 0,则malloc() 返回NULL或一个稍后可以成功传递给free()的唯一指针值。

  1. realloc(void *ptr,size_t size)

如果size>原来*ptr申请的空间大小,比如原来是100个字节,现在是150个字节,那么就有以下两种情况:

  1. 原来100个字节后还能放下50个字节,那么就在原来地址上增加50个字节,返回原地址

  2. 如果100个字节后面放不下50个字节,那么就会重新找个地址开辟150个字节空间,把原来的地址数据拷贝过来,释放掉原来地址空间,返回新地址。

如果size<原来*ptr申请的空间大小,比如原来100个字节,现在50个字节,那么就会释放掉后面50个字节

cpp 复制代码
int *p = (int*)malloc(sizeof(int)*25);
if( p == NULL)
{
    return -1;
}
printf("malloc %p\n",p);
//p = (int *)realloc(p,10);//重新分配 10 个字节大小,删除原来后面的 90 个字节
p = (int *)realloc(p,200);//重新分配 200 个字节大小,可能是在原来基础上加100,也可能是重新开辟 200
printf("malloc %p\n",p);
  1. calloc申请内存空间后,会自动初始化内存空间为 0

内存泄漏

什么是内存泄漏

内存泄漏就是申请了一块内存,使用完毕后没有释放,导致后续无法继续使用。

如何判断内存泄漏
  1. new/delete、malloc/free 一定要记得配套使用

  2. 将分配的内存的指针使用链表进行自管理,使用完毕后从链表删除

  3. C++可使用Boost 库中的 smart pointer智能指针

  4. 使用ccmalloc、Dmalloc、Leack等插件进行内存调试

指针、数组、引用

数组指针和指针数组有什么区别

数组指针就是指向数组的指针,重点是指针。例如

cpp 复制代码
int (*pa)[8]; //声明了一个指针,该指针指向一个有8个int型元素的数组

下面给出一个数组指针的示例。

cpp 复制代码
1	#include <stdio. h>
2	#include <stdlib. h>
3	void main()
4	{
5	int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
6	int (*p)[4]; //p是一个数组指针,它指向一个包含有4个int类型数组的指针
7	p = b;
8	printf("%d\n", **(++p); //程序输出结果为 5
9  }

指针数组就是存放指针的数组,重点是数组。例如

cpp 复制代码
int *p[4];//定义了一个存放int指针的数组

函数指针和指针函数有什么区别

  1. 函数指针

指向函数入口地址的指针称为函数指针。

cpp 复制代码
int (*p)(int , int); 

函数指针没有++和--运算

cpp 复制代码
#include <stdio.h>

// 定义一个函数,用于加法运算
int add(int a, int b) {
    return a + b;
}

// 定义一个函数,用于乘法运算
int multiply(int a, int b) {
    return a * b;
}

int main() {
    int result;
    int (*operation)(int, int); // 声明一个函数指针

    // 指向加法函数
    operation = &add;
    result = operation(3, 4);
    printf("Result of addition: %d\n", result);

    // 指向乘法函数
    operation = &multiply;
    result = operation(3, 4);
    printf("Result of multiplication: %d\n", result);

    return 0;
}
  1. 指针函数

返回值为指针类型的函数称为指针函数。

cpp 复制代码
int *p(int , int); 
int *(p(int, int));
//上面两种都是定义指针函数的方式
//*的优先级小于括号,p会先和右边的()结合,也就意味着p是函数

数组和指针的区别与联系是什么?

  1. 概念不同
    数组:是同种类型的集合
    指针:里面保存的地址的值
    2.赋值:
    同种类型指针之间可以直接赋值,数组只能一个个元素赋值
    3.存储方式:
    数组是连续的一段空间,指针的存储空间是不确定的
    4.修改内容不同
    比如:
    char p[ ] ="hello",我们可以执行 p[0] = 's'操作原因是 p 是数组可以基于下标修改数组内容
    Char * p = "hello" 执行 p[0] = 's'是错误的,因为 p 指向字符串常量,常量不允许修改

5.所占字节不同

指针永远是 4 个字节在 32 为系统中,而数组是不固定的,要看数组的类型和元素个数
6. 使用环境

指针多用于动态数据结构(如链表,等等)和动态内存开辟。

数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配(系统自动分配)。

指针进行强制类型转换后与地址进行加法运算,结果是什么

以一个结构体指针为例,将结构体指针强转为 ulong 类型后与地址进行加法运算:

当 p = 0x100000,则p+0x200=? (ulong)p+200=? (char*)p+0x200=?

p+0x200 = 0x100000 + 0x200*24 24为指针所指结构体的大小

指针加法,加出来的是指针所指类型的字节长度的整数倍,就是 p 偏移

sizeof(p)*0x200(ulong)p+200 = 0x10000000 + 0x200 = 0x10000200

经过ulong后变成数值加法了

(char*)p+0x200 = 0x1000000 + 0x200*sizeof( char ) 结果类型是char*

cpp 复制代码
struct BBB
{
    long num;
    char *name;
    short int data;
    char ha;
    short ba[5];
}*p;

/*在32位机器下,sizeof(struct BBB) = sizeof(*p) =

起始地址0~3  对齐long:4,满足首地址

起始地址4~7  对齐char* : 4,满足首地址

起始地址8~9 对齐short int:2,满足首地址

起始地址10    对齐1,满足首地址

起始地址12~21 对齐2,满足首地址

最终结构体大小为21,不满足最终大小整除结构体中基础数据类型的要求,故对齐为24*/

指针常量,常量指针,指向常量的常量指针有什么区别?

  1. 指针常量
cpp 复制代码
int * const p;

先看const 再看*,p是一个常量类型的指针,不能修改这个指针的指向,但是这个指针指向的地址上的存储值可以修改。

  1. 常量指针
cpp 复制代码
1	const int *p;
2	int const *p;

先看 *再看 const,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值。

  1. 指向常量的常量指针
cpp 复制代码
 const int *const p;

指向常量的常量指针,既不可以修改指针的值,也不可以修改指针指向的值。

指针的运算

例子 1 :

cpp 复制代码
int *ptr;   //假设指针指向的地址是 0x 00
Ptr++;      //运算之后指针指向 0x 04


char *p;
P++;        //地址偏移 1

//注意:对于一级指针的++操作偏移量要看指针指向的是什么类型
//对于二级指针偏移量,由于二级指针指向的是地址,所以固定是一个指针类型的大小,即在32位系统固定4个字节,

例子 2:

cpp 复制代码
#include<stdio.h>
int main()
{
    char a[20]="You_are_a_girl";
    char *p=a; char **ptr=&p;
    printf("**ptr=%c\n",**ptr);
    ptr++;
    printf("**ptr=%c\n",**ptr);
}
//在这个例子中是无法确定二级指针++之后指向的地址内容,因为二级指针(ptr)指针指向的一级指针的地址,
//如果二级指针(ptr)++之后,那么就会指向一级指针的后 4 个字节(对于 32 位操作系统来说指针类型是4 
//字节),至于这个地址里面是啥无从得知

*p++ *(p++) (*p)++ *++p ++*p

*p++和 *(p++) 一样,会先把*p 赋值给别人,再++地址

(*p)++变化的是地址上的值

*++p是先变化地址,再把地址上的值给别人

++*p地址不发生变化,值变化后再赋给别人

什么是野指针?如何产生?如何避免?

野指针是指指向未知地址的指针

产生原因通常是释放内存后,指针没有及时置空。

如何避免野指针:

  1. 声明指针变量时,初始化置NULL

  2. 释放内存后,将指针置NULL,避免悬挂

  3. 指针申请后,使用 assert 断言判空

cpp 复制代码
int *p1 = NULL;                     //初始化置 NULL
p1 = (int *)calloc(n, sizeof(int)); //申请 n 个 int 内存空间同时初始化为 0
assert(p1 != NULL);                 //判空,防错设计
free(p1);
p1 = NULL;                          //释放后置空
  1. 使用智能指针

什么是断言Assert ( int expression )

void assert( int expression );

expression--可以是一个变量或任何C表达式。如果expression为TRUE,assert() 不执行任何动作。如果expression 为FALSE,assert() 会在错误标准 stderr 上显示错误信息,并终止程序。

如何得到二维数组的行数?列数?

cpp 复制代码
int rows = sizeof(arr) / sizeof(arr[0]);
cpp 复制代码
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);

指针作为参数的注意点

典型易错点

指针实参和指针形参虽然指向的地址一样,但是自身的地址并不同!!!!!

对于一级指针*p作为参数swap(p,xxx)传入的是p指向的地址,&p才是p自身的地址

思考:传入参数a和b,a和b的地址是否交换?并没有!

cpp 复制代码
void swap(int *x, int *y)
{
    int *t;
 
    t = x;
    x = y;
    y = t;//指针指向的地址p、自身的地址&p、指针指向的首地址的值*p(数组就是第一个元素)
}

智能指针

什么是智能指针

在C++中,标准库提供了几种智能指针,包括:

  1. **std::unique_ptr**:独占所有权的智能指针。一个`std::unique_ptr`拥有对其指向对象的唯一所有权。当`std::unique_ptr`超出作用域时,它所管理的对象会被自动释放。

  2. **std::shared_ptr**:共享所有权的智能指针。多个`std::shared_ptr`可以共享对同一对象的所有权。当最后一个`std::shared_ptr`超出作用域时,对象会被释放。

  3. **std::weak_ptr**:弱引用智能指针。`std::weak_ptr`是为了解决`std::shared_ptr`的循环引用问题而引入的。它不会增加引用计数,因此不会影响对象的生命周期。

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

为了解决循环引用导致的内存泄漏,引入了weak_ptr。

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

数组名 num/&num 的区别

对于一维数组来说

num+1 是偏移到下个元素,&num+1是偏移整个数组

对于二维数组来说

num+1 是偏移到下个一维数组,&num+1 是偏移整个数组

指针和引用的区别是什么,如何相互转换

相同

  1. 指针指向某一内存,它的内容是所指内存的地址;引用则是某块内存的别名。

  2. 从内存分配上看:两者都占内存,程序为指针会分配内存,一般是 4 个字节。而引用的本质是指针常量,指向的地址不可以改变,而地址上的值可以改变

区别

  1. 指针是实体,而引用是别名

  2. 指针和引用的自增(++) 运算符意义不同,指针是对内存地址自增,而引用是对值的自增

  3. 引用使用时无需解引用,指针需要解引用(*)

  4. 引用是指针常量,只能在定义时被初始化一次,之后不可更改指向

  5. 引用不能为空,指针可以为空

  6. sizeof(引用)得到的是引用指向的对象的大小,sizeof(指针)得到的是指针本身大小(4字节)

转换

  1. 指针转引用:把指针用 * 就可以转换成对象,可以用在引用参数当中

  2. 引用转指针:把引用类型的对象用 & 取地址就获得指针了

cpp 复制代码
1	int a = 5;
2	int *p = &a;
3	void fun(int &x){}//此时调用fun可使用 : fun(*p);
4	//p是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值, (指针加上*就是指针转引用)
5	//p指针指向的对象会转换成引用X。 &引用就转指针了

二级指针和指针引用

cpp 复制代码
int a = 20;
int *p = &a; //创建一个指针,指针指向a的地址。等价于int *p;p=&a;
int *&q = p; //指针引用,指向一个指针p,所以q和p指向同一个地址

int **p = &p;
count << &a << endl;    //a的地址
count << &p << endl;    //一级指针自身地址
count << q << endl;     //二级指针指向的地址
count << q << endl;     //二级指针指向的地址里的值,即一级指针指向的地址
count << p << endl;     //一级指针指向的地址

数组的运算

以下代码表示什么意思?

cpp 复制代码
设有二维数组 a[][]

*(a[1]+1)、*(&a[1][1])、(*(a+1))[1]

*(a[1]+1)

因为 a[1] 是第 2 行的地址,a[1]+1偏移一个单位 (得到第2行第2列的地址),然后解引用取值,得到 a[1][1]

*(&a[1][1])

[ ] 优先级高,先对 a[1][1] 取地址再取值,还是 a[1][1] 的值

a+1 相当于&a[1],所以 *(a+1) = a[1],因此 *(a+1) [1] = a[1][1]

数组下标可以为负数吗?

可以,因为下标只是给出了一个与当前地址相对的偏移量而已

数组定义不能为负数,但访问可以用负数

p[-4] 代表数组元素中倒数第 5 个值

cpp 复制代码
1	#include <stdio.h>
2	int main()
3	{
4	    int i:
5	    int a[5]={0,1,2,3,4};
6	    int *p=&a[4]
7	    for(i=-4;i<=0;i++)
8	        printf("%d %x\n", p[i], &p[i]); //%x是unsigned int
9	    return O.
10  }
11	//输出结果为
12	//0 b3ecf480
13	//1 b3ecf484
14	//2 b3ecf488
15	//3 b3ecf48c
16	//4 b3ecf490

使用二维数组时注意传值和传地址的区别

局部变量每次初始化有可能被分配到同一地址

因此二维数组例如 s [ i ]= addname 时保存的时addname的地址值

由于局部变量每极大可能每次被分配到相同的地址,因此addname每次改变,例如scanf来输入addname时,都有可能导致 s [ i ] 的内容受影响

总之,二维数组第一层接收的是地址,如果不使用动态分配,极大可能每次给局部变量分配相同地址导致问题

cpp 复制代码
main(){
    //注意此处只初始化了前5个,其他的值和地址都没有被初始化,都是NULL
    char* ss[100] = { s[0],s[1],s[2],s[3],s[4] };
    ..................
    if(xxx)
    {
         //char addname[20] = "";//注意此处极大可能每次给局部变量分配相同地址导致问题
         //正确写法
         char addname[20] = (char*)malloc(20*sizeof(char));
         scanf("%s", addname);
         int re;
         re = add(addname, ss, p);
    }
    ..................    
}

int add(char* addname, char** ss, int* p) {

    ss[(*p)++] = addname; //!!!注意这里是将addname的地址传给ss[i]!!!
    return 1;
}

位操作

如何求解整型数的二进制表示中1的个数?

cpp 复制代码
1	#include <stdio.h>
2	int func(int x)  //功能函数: 求解二进制中1的而个数
3	{
4	    int countx = 0;
5	    while(x)         //当不为 0 的时候
6	    {
7	        countx++;    //计数
8	        x = x&(x-1); //把 x 对应的二进制数中的最后一位 1 去掉
9	    }
10	return countx;
11  }
12	int main()
13	{
14	    printf("%d\n",func(9999));
15	    return 0;
16  }

//比如 输入6 转换成二进制  0110
//6-1=5  转换成二进制就是  0101
//可以看到任何数-1以后   最右边的 1 右边位的值都会改变
//因此可以通过 x&(x-1)清除最右边的 1

如何求解二进制中 0 的个数

图解分析:

cpp 复制代码
#include <stdio.h>

int countZerosInBinary(int num) {
    int count = 0;
    
    while (num) {
        num = num & (num - 1);
        count++;
    }
    
    return count;
}

int main() {
    int num = 25; // 示例整数
    int zerosCount = countZerosInBinary(num);
    
    printf("Number of zeros in the binary representation of %d is: %d\n", num, zerosCount);
    
    return 0;
}

预处理

预处理标识#error的目的是什么?

#error预处理指令的作用是,编译程序时只要遇到#error就会生成一个编译错误提示消息,并停止编译。其语法格式为:#error error-message。

下面举个例子:

程序中往往有很多的预处理指令

cpp 复制代码
1	#ifdef XXX
2	...
3	#else
4	#endif

当程序比较大时,往往有些宏定义是在外部指定的(如 makefile),或是在系统头文件中指定的,当你不太确定是否定义了xxx宏定义时,可以使用

cpp 复制代码
1	#ifdef XXX
2	...
3	#error "XXX has been defined"
4	#else
5	#endif

这样,如果编译时出现了错误,输出了xxx has been define,表明xxx宏已经被定义了

定义常量谁更好?#define 还是 const?

#define 是单纯的文本替换 ,#define常量的生命周期止于编译期,不分配内存空间 ,它存在于程序的代码段,在实际程序中,它只是一个常数;

而 const 常量存在于程序的数据段 ,并在堆、栈中分配了空间,const常量可被调用、传递;const常量有数据类型,编译器可以对其进行安全检查,而define常量不能

typedef和define有什么区别

typedef 与 define都是替一个变量取别名,以此来增强程序的可读性

#define是 c 语言定义的语法,只是预处理命令,只做简单的字符串替换不具备安全性检查。同时宏定义不具有作用域限制,只要是前面任何地方定义过的宏定义,后续都可以使用。

typedef是关键字,在编译时期处理,具备安全性检查功能。例如,typedef int Integer;这样以后就可以用 Integer 代替 int 作整形变量的类型说明了。typedef有作用域限制。

如何使用define声明个常数,用以表明1年中有多少秒(忽略闰年问题)

cpp 复制代码
#define SECOND_PER_YEAR (60*60*24*365)UL

# include <filename.h>和# include "filename.h"有什么区别

对于尖括号包裹的头文件,编译器先从标准库路径开始搜索,使得系统文件调用较快。

对于扩号包裹的头文件,编译器从用户的工作路径开始搜索,然后去找系统路径,使得自定义文件调用较快。

在头文件中定义静态变量是否可行,为什么

不可行,如果在头文件中定义静态变量,对于每个引用该程序的源文件来说,都会存在一个独立的静态变量,造成空间的浪费

写一个标准宏MIN,这个宏输入两个参数并返回较小的一个

cpp 复制代码
1 #define MIN(A,B) ((A) <= (B) ? (A) : (B))

不使用流程控制语句,打印出1~1000的整数

cpp 复制代码
1	#include<stdio. h>
2	#define A(x)x;x;x;x;x;x;x;x;x;
3	int main ()
4	{
5	int n=1;
6	A(A(A(printf("%d", n++);
7	return 0;
8  }

变量

全局变量和局部变量的区别是什么?

全局变量是在函数外定义的变量

  1. 全局变量的作用域为程序块,局部变量的作用域为当前函数

  2. 全局变量和静态变量(全局静态变量、局部静态变量)都分配在数据区的静态存储空间。而局部变量分配在栈上。

  3. 全局变量生命周期伴随主程序的全过程,而局部变量生命周期随函数。

static变量可不可以定义在多个.c文件中?

可以。static的作用域只限于当前.c文件,不同.c文件中定义同名Static变量其实各自是不同个体。

局部变量能否和全局变量重名?

可以。C语言中局部变量会屏蔽全局变量。

对于外部变量和局部变量重名的情况,如果要把外部变量赋给局部变量,C++中可以采用this指针调用外部变量,C语言无解决方案。

函数

C语言是如何进行函数调用的

大多数CPU上的程序使用栈帧来支持函数调用操作。

每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针
栈帧结构示意图

栈指针和帧指针一般都有专门的寄存器,

通常使用ebp寄存器作为帧指针,使用esp寄存器作为栈指针

帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶

如何让函数在main函数执行前或执行后运行

GNU项目对函数属性的主要设置关键字如下:

alias: 设置函数别名

aligned:设置函数对齐方式

always_inline/gnu_inline:函数是否是内联函数

constructor/destructor:主函数执行之前/之后执行的函数

format:指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置

noreturn:指定这个函数没有返回值。类似_exit/exit/aboard

weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定函数名称有冲突,使用全局函数名称

attribute ((constructor))是GCC编译器提供的特性。该特性可支持函数在加载时自动运行,即在main函数之前运行

示例代码如下:

cpp 复制代码
#include <stdio.h>

void before()   _attribute_  ((constructor)); 
void after()   _attribute_  ((destructor));

void before() {
printf("this is function %s\n",  func  ); return;
}


void after(){
printf("this is function %s\n",  func  ); return;
}


int main(){
printf("this is function %s\n",  func  ); return 0;
}

// 输出结果
// this is function before
// this is function main
// this is function after

C++为什么可能被继承并用作基类的类的析构函数必须是虚函数?

如果一个类的析构函数是虚函数,为了支持运行时的动态绑定,编译器会为该类的每个对象添加一个虚表指针,用来指向虚函数表。虚函数表用于在继承关系中正确调用派生类的虚函数。

cpp 复制代码
#include <iostream>

//定义基类
class Base {
public:
    //基类的析构函数被设置为虚函数
    virtual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};

//定义子类
class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();  //通过基类指针指向子类对象
    delete ptr;                 // 释放基类指针

    return 0;
}

为什么C++默认的析构函数不是虚函数

虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,动态绑定和多态性的概念就不适用于该类,因为不会有其他类继承它并覆盖其虚函数。

静态函数和虚函数的区别

静态函数在编译时就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

重载和重写(覆盖)

重写:

是指派生类中存在重写函数,函数名、参数、返回值类型必须和基类中被重写的函数一样,只是它们的函数体不一样,被重写的函数必须用到 virtual 修饰

例如:

cpp 复制代码
Class A{
Public:
Virtual void fun(int a){ cout << "this A "; }
};
Class B :public A{
Public:
void fun(int a){cout << "this B ";}
}

派生类对象调用时会调用派生类的重写函数,不会调用父类的被重写函数

重载:

是指函数名相同,函数参数或返回值类型不同

cpp 复制代码
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};

虚函数表怎么实现运行时多态

首先虚函数表是一个类的虚函数的地址,每个对象在创建时,都会有一个指针指向该类的虚函数表 ,每一个类的虚函数表按照函数声明顺序,会将函数地址存在虚函数表里当子类对象重写父类的虚函数时 ,该对象父类的虚函数表中对应的虚函数的地址就会被子类的虚函数地址覆盖

构造函数有几种,分别什么作用

默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数

  1. 默认构造函数和初始化构造函数,在定义类的对象的时候,完成对象的初始化工作

有了有参的构造函数后,编译器就不提供默认的无参构造函数

cpp 复制代码
class Student{
public:
//默认构造函数
Student() { num=1001; age=18; }
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main(){
//用默认构造函数初始化对象 S1
Student s1;
//用初始化构造函数初始化对象 S2
Student s2(1002,18);
return 0;
}
  1. 拷贝构造函数

拷贝构造函数起的作用就是深拷贝。

浅拷贝在复制的时候,对象里的指针仍然指向原来的地址。

深拷贝在复制的时候,指针的内容会被复制,但是指针的地址会是新的。

cpp 复制代码
#include "stdafx.h"
#include "iostream.h"
class Test
{
    int i;
    int *p;
    public:
        Test(int ai,int value)
        {
            i = ai;
            p = new int(value);
        }

    ~Test()
        {
            delete p;
        }

    Test(const Test& t) //拷贝构造
        {
            this->i = t.i;
            this->p = new int(*t.p);
        }
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
    Test t1(1,2);
    Test t2(t1);//将对象 t1 复制给 t2。注意复制和赋值的概念不同
    return 0;
}
  1. 移动构造函数,用于将其他类型的变量,隐式转换为本类对象

只定义析构函数,会自动生成哪些构造函数

编译器会自动生成拷贝构造函数和默认构造函数

说说一个类,默认会生成哪些函数

无参构造函数、

拷贝构造函数、拷贝赋值运算符、

移动构造函数、移动赋值运算符、

析构函数(非虚)

说说C++类对象的初始化顺序、有多重继承情况下的初始化顺序

父类构造函数->成员类对象构造函数->自身构造函数

->自身析构函数->成员类对象析构函数->父类析构函数

cpp 复制代码
#include <iostream>

class Base1 {
public:
    Base1() { std::cout << "Base1 constructor" << std::endl; }
    ~Base1() { std::cout << "Base1 destructor" << std::endl; }
};

class Base2 {
public:
    Base2() { std::cout << "Base2 constructor" << std::endl; }
    ~Base2() { std::cout << "Base2 destructor" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    Derived() { std::cout << "Derived constructor" << std::endl; }
    ~Derived() { std::cout << "Derived destructor" << std::endl; }
};

int main() {
    Derived d;
    return 0;
}

/*
`Derived` 类继承自 `Base1` 和 `Base2`。
根据上述规则,`Derived` 对象的构造顺序将按照 `Base1`、`Base2`、`Derived` 的顺序进行初始化,
析构顺序将按照 `Derived`、`Base2`、`Base1` 的顺序进行调用。
*/

Select函数

作用:监听设置的 fd 集合

工作流程:

会从用户空间拷贝 fd_set 到内核空间,然后在内核中遍历一遍所有的 socket 描述符,如果没有满足条件的socket描述符,内核将进行休眠,当设备驱动发生时自身资源可读写后,会唤醒其等待队列上睡眠的内核进程,即在 socket 可读写时唤醒,或者在超时后唤醒

  1. select函数原型
cpp 复制代码
/*
maxfdp1 指定感兴趣的 socket 描述符个数,它的值是套接字最大 socket 描述符加 1
        socket 描述符 0、1、2 ...maxfdp1-1 均将被设置为感兴趣(即会查看他们是否可读、可写),注意                 
        0,1,2 会事先被设置为感兴趣,也就是说我们自己的 fd 是从 3 开始
下面几个参数设置什么情况下会返回:
readfds:指定这个 socket 描述符是可读的时候才返回。
writeset:指定这个 socket 描述符是可写的时候才返回。
exceptset:指定这个 socket 描述符是异常条件时候才返回。
timeout:指定了超时的时间,当超时了也会返回。
注意:如果对某一个条件不感兴趣,就可以把它设置为空指针。
返回值:就绪 socket 描述符的数目,超时返回 0,出错返回-1。
*/
1 int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
  1. 文件描述符的数量

单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量;(在Linux内核头文件中定义:#define _FD_SETSIZE 1024)

  1. 就绪fd采用轮询的方式扫描

select 返回的是 int,可以理解为返回的是ready(准备好的)一个或多个文件描述符,应用程序需要遍历整个文件描述符数组才能发现哪些 fd 句柄发生了事件,由于select采用轮询的方式扫描文件描述符(不知道哪个文件描述符读写数据,所以把所有的 fd 都遍历),文件描述符数量越多性能越差

  1. 内核/用户空间内存拷贝

select 每次都会改变内核中的句柄数据结构集( fd 集合),因而每次调用select都需要从用户空间向内核空间复制所有的句柄数据结构( fd集合 ),产生巨大的开销4

  1. select的触发方式

select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次调用 select 还是会将这些文件描述符通知进程

  1. 优缺点
  • select 的可移植性较好,可以跨平台;
  • select 可设置的监听时间 timeout精度更好,可精确到微秒,而 poll 为毫秒
  • select支持的文件描述符数量上限固定为1024,不能根据用户需求进行更改;
  • select每次调用时都要将文件描述符集合 fd_set 从用户态拷贝到内核态,开销较大;
  • 每次在 select() 函数返回后,都需要通过遍历文件描述符来获取已经就绪的 socket

文件描述符集合操作

文件描述符集合的所有操作都可以通过这四个宏来完成,这些宏定义如下所示

cpp 复制代码
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

这些宏按照如下方式工作:

  • FD_ZERO() 将参数 set 所指向的集合初始化为空;

  • FD_SET() 将文件描述符 fd 添加到参数 set 所指向的集合中;

  • FD_CLR() 将文件描述符 fd 从参数 set 所指向的集合中移除;

  • 如果文件描述符 fd 是参数 set 所指向的集合中的成员,则 FD_ISSET() 返回 true,否则返回 false,一般用来判断返回的文件描述符是否为目标文件描述符

注意事项:

每次调用 select()函数 时,当监听到某个文件描述符可读或写或异常时,只把该文件描述符返回,也就是说,会修改我们前面设置的监听集合

举个例子:
fd_set rdfds;

My_rdfds = rdfds;

FD_ZERO ( &rdfds );

FD_SET ( 0 , &rdfds ); //添加键盘

FD_SET ( fd, &rdfds ); //添加鼠标

ret = select ( fd + 1, &My_rdfds, NULL, NULL, NULL );

每次监听都是监听 My_rdfds,这样就可以保证 rdfds 是不变的

Fork Wait Exec函数

父进程通过 fork 函数创建一个子进程,此时这个子 1 进程知识拷贝了父进程的页表,两个进程都读同一个内存,exec 函数可以加载一个 elf 文件去替换父进程,从此子进程就可以运行不同的程序。fork 从父进程返回子进程的 pid,从子进程返回0.调用了 wait 的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回 -1。exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回 -1。

cpp 复制代码
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
wait 和 waitoid 的区别在于,前者不能等待指定的 pid 子进程

-Pid
    pid < -1 等待进程组识别码为 pid 绝对值的任何子进程
    pid = -1 等待任何子今晨,相当于 wait();
    pid = 0  等待进程组识别码与目前进程相同的任何子进程

-options的说明
    WNOHANG 若 pid 指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的id
    WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予理会。WIFSTOPPED(status)宏确定返回是否对应于一个暂停子进程
    子进程结束状态返回后存于 status,底下有几个宏可判别结束情况
    WIFEXITED(status)如果为正常结束子进程的返回状态,则为真,对于这种情况可执行WEXITSTATUS,取子进程传给exit或_exit的低 8位。
    WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束才能使用此宏。
    WIFSIGNALED(status)若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
    WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED来判断后才使用此宏。
    WIFSTOPPED(status)若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
    WSTOPSIG(status)取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED来判断后才使用此宏。
    如果执行成功则返回子进程识别码(PID),如果有错误发生则返回
    返回值-1。失败原因存于errno 中。

select epoll poll函数的区别

1) select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。

而epoll其实也需要调用epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在"醒着"的时候要遍历整个 fd 集合,而 epoll 在"醒着"的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。

2) select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列)。这也能 节省不少的开销。

变参函数的实现

**`template<typename T, typename... Args>`**是C++中的模板参数包(template parameter pack)的语法。这个语法允许在模板中接受可变数量的模板参数。

在这里,`T` 是模板的第一个参数,而 `Args` 是一个模板参数包,表示零个或多个附加的模板参数。这种语法通常用于定义接受可变数量参数的模板,允许在模板实例化时传入不固定数量的参数。

cpp 复制代码
#include <iostream> // 基本情况:当没有参数时结束递归 
void printArgs() 
{ 
    std::cout << std::endl; 
}
// 递归情况:打印第一个参数,然后调用自身处理剩余参数
template<typename T, typename... Args>
void printArgs(T firstArg, Args... args) {
    std::cout << firstArg << " ";
    printArgs(args...);
}

 int main() 
{ 
    printArgs(1, "Hello", 3.14, 'A'); 
    return 0;
}

字符输入函数

英文一个字符占1个字节,汉字一个字符占3个字节

fputc、putc、putchar 返回字符的ASCII码,失败返回EOF宏(-1)

puts、fputs 返回非负数

fseek函数

作用:用于在文件中移动位置指针到特定的位置

int fseek ( FILE *stream, long offset, int whence );

//参数:文件流,偏移量,起始位置

文件位置函数

ftell() 函数用于得到文件位置指针当前位置相对于文件首的偏移字节数

fseek()函数用于设置文件指针的位置

rewind()函数用于将文件内部的位置指针重新指向一个流(数据流/文件)的开头

ferror()函数可以用于检查调用输入输出函数时出现的错误。

int fseek ( FILE *stream, long offset, int whence );

long ftell ( FILE *stream );

void rewind ( FILE *stream );

int fgetpos ( FILE *stream, fpos_t *pos );该函数相当于 ftell

int fsetpos ( FILE *stream, const fpos_t *pos );该函数相当于 fseek

定义处理信号的函数

cpp 复制代码
typedef void ( *sighandler_t ) ( int );
sighandler_t signal ( int signum, sighandler_t handler );
/*利用typedef定义的`sighandler_t` 是一个函数指针类型,
指向一个没有返回值且接受一个整型参数的函数。
在C语言中,这种类型通常用于注册信号处理函数,
以便在程序接收到信号时执行相应的操作。*/

printf() 函数和 scanf() 函数中的 *

* 在 printf() 中表示占位符
* 在 scanf() 中表示忽略输入

当我们输入 12 34 的时候会自动忽略 12 把 34 作为 x 的值,所以相当于 y 没有输入

输出的时候由于使用了占位符,5 表示占 5 位置,所以如果 x 不够 5 位那么就会补 3 位,如果大于 5 位那就直接输出 x

文件IO/标准IO(Linux-C)

文件IO和标准IO的区别

区别一:标准IO来源于C语言标准库,文件IO来源于LINUX内核

区别二:标准IO是对文件IO的封装,主要在于缓冲区的实现减少了用户态和内核态的切换。标准IO有缓冲(全缓冲、行缓冲、不缓冲),文件IO无缓冲

区别三:标准IO操作文件的入口是文件流,文件IO操作文件的入口是文件描述符。

标准IO任何操作系统皆可调用,但需注意例如 Windows 使用回车符和换行符 `\r\n`,而 Unix/Linux 使用换行符 `\n`。不同操作系统使用不同的文件路径分隔符,例如 Windows 使用反斜杠 `\`,而 Unix/Linux 使用正斜杠 `/`。

系统调用:操作系统给我们提供的接口,会导致用户态和内核态的切换。

printf 可以调用内核中的接口 直接驱动显卡运行

printf实际是库函数中的函数,然后调用系统调用

原因:针对不同的操作系统,库函数可以翻译后成标准的系统调用对接不同的系统(安卓/linux)

strlen() memcpy() 等函数 也是靠库函数实现

文件IO是直接使用系统调用的

系统调用就是操作系统提供的内核接口函数,将系统调用封装成库函数可以起到隔离内核的作用,提高程序的可移植性,printf就是库函数封装了内核系统调用接口才能在显示器上显示字符

文件流分为文本流和二进制流,还可分为输入流和输出流

标准IO缓冲区的概念:

标准IO有缓冲(全缓冲,行缓冲,无缓冲),文件IO无缓冲

全缓冲:缓冲区满才输出

行缓冲:遇到换行符输出

Windos和Linux换行符区别

Windos是\r\n

Linux是\n

常见的文件IO和标准IO

|-------|-------------------------------------------|-----------------------------------------------|
| | 文件IO | 标准IO |
| C | open()-- 返回设备描述符 fwrite() fread() lseek() | fopen()---返回FILE write() read() puts() gets() |
| C++ | C++和C区别较大,见独立内容 ||

C语言下常见文件IO

open()

write()/read()、pread()

lseek()

打开文件:

int open(const char *pathname, int flags, mode_t mode);//文件路径,读写方式,权限模式

open()函数返回一个非负整数的文件描述符(file descriptor),用于后续对文件的读写操作。如果打开文件失败,函数返回-1,并可以使用errno.h文件中的errno全局变量获取具体的错误码。 文件描述符可理解为文件编号 #include <fcntl.h>
读写方式:

O_CREAT:在文件打开过程中创建新文件

O_RDONLY:以只读方式打开文件

O_WRONLY:以只写方式打开文件

O_RDWR:以读写方式打开文件

O_APPEND:在文件末尾追加数据,而不是覆盖现有内容

O_TRUNC:如果文件已经存在,将其截断为空文件

O_EXCL:与 O_CREAT 一起使用时,如果文件已经存在,则 open() 调用将失败O_SYNC:使文件写操作变为同步写入,即将数据立即写入磁盘

O_NONBLOCK:以非阻塞方式打开文件,即使无法立即进行读写操作也不会被
权限模式:4可写 2可读 1可执行

用户 组 其他

例子:731 代表 用户权限为4+2+1、组权限为2+1、其他用户权限为1

cpp 复制代码
#include <fcntl.h>
#include <stdio.h>
 
int main() {
    int fd = open("file.txt", O_RDONLY|O_CREATE|O_TRUNC,0644);
    if (fd == -1) {
        perror("open error");
        return 1;
    }
 
    // 对文件进行读取或写入操作
    close(fd); // 关闭文件
 
    return 0;
}

写入文件

ssize_t write(int fd, const void *buf, size_t count); //返回写入的字节数

fd为文件描述符,buf指向要写入数据的缓冲区,count为要写入的字节数 #<unistd.h>

cpp 复制代码
    //打开指定文件,若不存在则创建。
    int fd=open("./1.txt",O_RDWR|O_CREAT,0777);
    //向指定文件写入内容
    char *str="WoNiuXueYuan";
    write(fd,str,strlen(str));
    printf("write success!\n");

读取文件,返回读取到的字节数,错误返回-1

ssize_t read(int fd, void *buf, size_t count);

fd为文件描述符,buf指向要读取的数据缓冲区,count为要写入的字节数 #<unistd.h>

从偏移量offset处读取文件,返回读取到的字节数,错误返回-1

ssize_t pread(int fd, void *buf, size_t count, off_t offset);//offeset为相对文件开头的偏移量

使用read函数读取文件时,每次读取后,文件指针会自动向后移动相应的字节数;而使用pread函数读取文件时,文件指针不会改变,仍然指向之前的位置。

cpp 复制代码
    //打开指定文件,若不存在则创建。
    int fd=open("./1.txt",O_RDWR|O_CREAT,0777);    
    //从指定文件读取内容
    char buf[100];
    read(fd,buf,100);
    printf("buf=%s\n",buf);

偏移文件读写位置,返回当前读写位置,错误返回-1 <sys/types.h> <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

fd是文件描述符,

offset是指针偏移量(字节为单位),

whence偏移量对应的参考数值(宏定义)
偏移量参考值:

SEEK_SET:读写偏移量指向offset字节位置处(从文件头部开始算)

SEEK_CUR:读写偏移量将指向当前位置+偏移量参数

SEEK_END:读写偏移量指向文件末尾+offset字节位置处

cpp 复制代码
//打开指定文件,若不存在则创建。
    int fd=open("./1.txt",O_RDWR|O_CREAT,0777); 
//从起始位0偏移12位,作为新的读写起始位。
    lseek(fd,12,0);
//后续读写操作都是从新起始位开始的,适用于追加等需求。

C语言下常见标准IO

fopen()

fwrite()/fread()

fputs()/fgets() //fgets行读取,碰到\n结束

打开文件 fopen()

FILE *fopen(const char *filename, const char *mode); 成功返回FILE文件流 失败返回NULL

文件名/文件路径 操作模式
"r" 只读

"r+" 读写,若不存在则报错

"w" 只写,若存在则清零,若不存在则创建

"w+" 读写,若存在则清零,若不存在则创建

"a" 追加,若不存在则创建,存在则追加(EOF符保留)

"a+" 追加,若不存在则创建,存在则追加(EOF符不保留)

cpp 复制代码
 FILE *fp=fopen("./2.txt","r+");

写入文件 fwrite()

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);//返回写入元素总数

  • ptr-- 这是指向用于写入的元素数组的指针。

  • size-- 这是要被写入元素的大小,以字节为单位。

  • nmemb-- 这是元素的个数,每个元素的大小为 size 字节。

  • stream-- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

cpp 复制代码
    FILE *fp=fopen("./2.txt","r+");
     *str="hello";
    fwrite(str,strlen(str),1,fp);
    printf("write success!\n");
    fclose(fp);

读取文件 fread()

size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );

  • buffer-- 指向要读取的数组中首个对象的指针
  • size-- 每个对象的大小(单位是字节)
  • count-- 要读取的对象个数
  • stream-- FILE输入流
cpp 复制代码
    FILE *fp=fopen("./2.txt","r+");

    char buf[100];
    fread(buf,100,1,fp);
    printf("buf=%s\n",buf);
    fclose(fp);
    return 0;

写入文件 fputs() //适用于文本写入,遇到\0空字符结束 而fwrite适用各类写入

int fputs(const char *str, FILE *stream); //成功返回非负,否则返回EOF
读取文件行 fgets()

char *fgets(char *str, int n, FILE *stream);

  • str-- 缓冲区指针

  • n-- 这是要读取的最大字符数(包括最后的空字符)

  • stream-- FILE 对象字符流

cpp 复制代码
int main(){
    FILE *fp=fopen("./2.txt","r+");
    while(1){
        char row[50];
        fgets(row,50,fp);
        //读到末尾了,跳出。
        if(feof(fp)) break;
        printf("row=%s",row);
    }
    fclose(fp);
    return 0;
}

什么是文件描述符?

在Unix-like操作系统中,文件描述符(file descriptor)是用于标识打开文件或I/O设备的整数值。

每个进程在其打开的文件或设备上都有一组文件描述符,它们是连续的、非重复的整数值。当一个文件或设备被打开时,操作系统会为该文件分配一个文件描述符,并将其返回给应用程序。

在C语言中,文件描述符被表示为int类型。常用的文件操作函数(如open()、read()、write()、close()等)使用文件描述符作为参数来指定要操作的文件。

常见的文件描述符包括:

标准输入(stdin):文件描述符为0,宏为STDIN_FILENO,通常用于接收应用程序的输入。

标准输出(stdout):文件描述符为1,宏为STDOUT_FILENO,通常用于输出应用程序的结果。

标准错误(stderr):文件描述符为2,宏为STDERR_FILENO,通常用于输出应用程序的错误信息。

应用程序可以使用文件描述符进行各种文件和I/O操作,例如读取文件内容、写入数据、关闭文件等。文件描述符作为操作系统和应用程序之间的桥梁,允许应用程序与文件系统和其他设备交互。

不同操作系统和编程环境可能对文件描述符的具体取值范围和含义有所不同。

面向对象

面向对象和面向过程的区别

面向过程:依据业务逻辑从上到下写代码

面向对象:将数据与函数绑定到一起,进行封装

面向对象的三大特征

封装:将对象或函数封装起来,仅对外提供限定的接口。

继承:对象的一个新类可以从现有的类中派生。继承方式包括公有、私有、保护。子类无论以哪种方式继承都无法操作基类的私有成员。私有继承会使基类的公有成员、保护成员变为子类的私有成员;保护继承会使基类的公有成员、保护成员变为子类的保护成员

多态

用父类指针指向子类的实例,然后通过父类指针调用子类的成员函数,一般有重写、重载。

重写是动态多态(运行期完成),重载是静态多态(编译器在编译器完成)

重写需要满足条件:

1)虚函数。基类中必须有基函数,在派生类中必须重写虚函数。

2)通过子类的实例(可用父类指针指向该实例,也可用子类指针指向该实例)调用重写的函数

什么是深拷贝?什么是浅拷贝?

对一个已知对象进行拷贝,编译系统会自动调用拷贝构造函数。

若用户没有自定义拷贝构造函数,则会调用默认拷贝构造函数,进行的是浅拷贝。浅拷贝可能造成多个对象操作同一块空间的错误。

在对含有指针对象的成员进行拷贝时,必须要自定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即深拷贝。

什么是友元?

在C++中,友元(friend)是一种机制,允许一个类或函数访问另一个类的私有成员。

友元有两种形式:友元类和友元函数,通过 friend关键字 定义

cpp 复制代码
class MyClass {
private:
    int privateVar;

public:
    MyClass() : privateVar(0) {}
    friend void friendFunction(MyClass obj); // 声明友元函数
    friend class FriendClass; // 声明友元类
};

void friendFunction(MyClass obj) {
    obj.privateVar = 10; // 友元函数可以访问 MyClass 的私有成员 privateVar
}

class FriendClass {
public:
    void modifyPrivateVar(MyClass& obj) {
        obj.privateVar = 10; // 友元类可以访问 MyClass 的私有成员 privateVar
    }
};

int main() {
    MyClass myObj;
    friendFunction(myObj);

    FriendClass friendObj;
    friendObj.modifyPrivateVar(myObj);

    return 0;
}

什么函数不能声明为虚函数

普通函数(非成员函数),构造函数,静态成员函数,内联函数,友元函数

一句话,只要不能被继承的函数,都不能被声明为虚函数

普通函数(非成员函数)只能被重载,不能被重写,声明为虚函数没意义
构造函数是为了明确初始化对象成员而产生;虚函数是为了不明确基类的成员细节而产生
内联函数在编译时展开,用于减少函数调用开销;虚函数运行时才能动态绑定
静态成员函数对每个对象来说只有一份,没有动态绑定的必要性
友元函数不支持继承,也就不支持多态,也就不需要虚函数

Vector的底层实现

vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。

当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器都失效了。

vector维护的是一个连续的线性空间,所以不论其元素类型为何,普通指针都可以作为vector的迭代器而满足所以必要条件,如operator*,operator->,operator++,operator--,普通指针天生就具备。vector支持随机存取,而普通指针正有这样的能力。所以底层直接将指针封装成了iterator。

数据结构与算法

数组

链表

栈是用数组实现的

队列

排序算法及其改进方法

快速排序

cpp 复制代码
int *QuickSort(Node* pBegin, Node* pEnd){
   if(pBegin == NULL || pEnd == NULL || pBegin == pEnd){
        return 0;
     }
   //定义两个指针
    Node* p1 = pBegin;
    Node* p2 = pBegin->next;
}

网络编程

1. TCP头部结构

TCP固定头部结构

每个TCP报文段都包含着此报文段的TCP头部信息,用于指定源端端口、目的端端口以及管理TCP连接等。完整的TCP头部结构可分为固定头部结构和头部选项两个部分。

  • 32位端口号:包括了16位源端口号和16位目的端口号。
  • 32位序号:假设第一次主机A发送给主机B的TCP报文段序号为随机的ISN(初始序号值),则下一次A到B方向上报文段的序号值为ISN+此次报文段所携带数据在整个字节流中的偏移量
  • 32位确认号:收到对方的报文段的序号值加1
  • 4位头部长度+6位标志位+16位窗口大小:

4位头部长度标识该TCP头部有多少个4字节(最大15×4=60字节)。

6位标志位包含如下几项:

URG紧急标志位 表示紧急指针是否有效

ACK确认标记位 表示确认号是否有效。携带ACK的称为确认报文段

PSH优先标记位 表示接收端立即从接收缓冲区读走数据

RST断开连接标记位 表示要求重建连接。携带RST标志的称为复位报文段

SYN请求标记位 表示请求建立一个连接。携带SYN标志的称为同步报文段

FIN结束标记位 表示请求关闭连接。携带FIN标志的为结束报文段。

16位窗口大小:流量控制。告诉对方本端的接收缓冲区还能容纳多少字节的数据

  • 16位校验和+16位紧急指针:

校验和由发送端填充。接收端通过CRC奇偶校验检查数据完整性

紧急指针标识紧急字节在数据流的首地址

TCP头部选项字段

TCP头部最后的选项字段最多40字节,因为TCP头部最多60字节。

TCP头部选项的一般结构如图:

1字节的选项类型+1字节选项的总长度+n字节选项的具体信息

常见的TCP选项有7种:

kind = 0是选项表结束选项。

kind = 1是空操作选项,一般用于将TCP选项的总长度填充为4字节整数倍。

kind = 2是最大报文长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS)

kind = 3是窗口扩大因子选项。和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。

kind = 4是选择性确认选项。在连接初始化时,表示是否支持SACK技术。使得TCP模块报文段丢失时只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。

kind = 5是SACK实际工作选项。该选项的参数告诉发送方本端已经收到且已经缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。

kind = 8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。

请说一下socket网络编程中客户端和服务端用到哪些函数?

1)服务器端函数

//套接字描述符可以像文件描述符一样read()

socket() 返回套接字描述符

int socket(int domain, int type, int protocol);

协议域

AF_INET IPv4 地址族

AF_INET6 IPv6 地址族

AF_UNIX UNIX 域套接字,用于在同一台计算机上的进程间通信。

AF_PACKET 原始套接字,用于对数据链路层数据包进行直接操作。

AF_NETLINK 用于与 Linux 内核通信的协议域

套接字类型

SOCK_STREAM 字节流套接字(TCP)

SOCK_DGRAM 数据报套接字(UDP)

SOCK_RAW 原始套接字,用于直接访问网络层数据包(未经协议栈处理的)

SOCK_SEQPACKET 字节流套接字,但保留了数据包i的边界

SOCK_RDM UDP,但保证数据不会重复

协议

0 系统根据地址域和套接字类型自动选择合适的协议

IPPROTO_TCP 指定使用 TCP协议

IPPROTO_UDP 指定使用UDP协议

IPPROTO_ICMP 指定使用ICMP协议。通常用于网络诊断

IPPROTO_RAW 指定使用原始IP协议。允许直接访问网络层数据包
bind() 将地址与套接字绑定

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//错误返回负数

套接字描述符

socketaddr结构体指针

在实际的 Socket 编程中,通常会用 `sockaddr_in` 结构体来存储网络地址信息,

然后将其转换为通用的 `sockaddr` 结构体传递给 Socket 函数,

如 `bind()`、`connect()`、`accept()` 等函数。

struct sockaddr {

unsigned short sa_family; // 地址族。AF_INET表示IPV4

char sa_data[14]; // 地址数据,包括地址信息

};

struct in_addr {

in_addr_t s_addr; // IPv4 地址

};

struct sockaddr_in {

short sin_family; // 地址族,AF_INET表示IPV4

unsigned short sin_port; // 端口号 如 htons(8080)

struct in_addr sin_addr; // IPv4 地址

// INADDR_ANY 接收任意地址的连接

// INADDR_LOOPBACK 本机自身的地址

char sin_zero[8]; // 未使用,填充字段

};

socketaddr结构体长度

用sizeof()得到
listen() 将套接字设为监听模式,等待连接请求。

int listen(int sockfd, int backlog);//一直监听直到发生错误或有连接到来。

套接字描述符

同时可以处理的连接请求数量
accept() 接收客户端连接请求,返回一个新的套接字用于数据收发

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

套接字描述符

socektaddr结构体指针

socketaddr结构体长度
connect() 发起与远程主机的连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

套接字描述符

sockaddr结构体指针

socketaddr结构体指针长度
send() 向socket发送数据 //文件Io的write也可以用 不会携带\0

ssize_t send(int sockfd, const void *buf, size_t len, int flags);//返回发送成功的字节,失败-1

套接字描述符

发送缓冲区指针 //如果是传结果体可以直接&struct来序列化

要发送的字节数

指定发送操作的选项

recv() 从socket读取数据 //文件Io的read也可以用 均不会自动给添加结束符'\0'

ssize_t recv(int sockfd, void *buf, size_t len, int flags);//flags=0代表默认阻塞

recv默认阻塞,buffer值NULL则等待,可用一些包的函数设置套接字阻塞/非阻塞

返回接收到的字节数,返回0代表连接关闭,返回-1代表发生错误,可通过errno调出返回值

perror(errno)//打印错误码 printf("%d",errno)也可

套接字描述符

接收缓冲区指针

要接收的字节数

指定接收操作的选项
close 关闭socket连接

int close(int fd); //关闭成功返回0,失败返回-1

套接字描述符

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define PORT 8080
/*服务端*/
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    char *hello = "Hello from server";

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;         //设置地址族
    address.sin_addr.s_addr = INADDR_ANY;
  
    //将主机字节序的16位无符号整数转换为网络字节序。不同系统可能使用不同大小端字节序。
    address.sin_port = htons(PORT);

    // 绑定套接字到指定端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听传入的连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 从客户端接收数据
    read(new_socket, buffer, 1024);
    printf("Client: %s\n", buffer);

    // 向客户端发送数据
    send(new_socket, hello, strlen(hello), 0);
    printf("Hello message sent\n");

    return 0;
}

/*
`perror` 是一个 C 标准库函数,
用于将由标准库函数产生的错误代码(保存在全局变量 `errno` 中)打印成人类可读的错误消息
到标准错误输出(stderr)。它的原型定义在 `<stdio.h>` 头文件中。
*/
/*
`socklen_t` 是一个数据类型,通常用于表示套接字地址结构的长度。
它通常被定义为 `unsigned int` 或者 `unsigned long` 类型,足够大以容纳套接字地址结构的长度
*/
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define PORT 8080

int main() {
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};
    int sock = 0;

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);    //将主机字节序转换为网络字节序
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }

    // 向服务器发送数据
    send(sock , hello , strlen(hello) , 0 );
    printf("Hello message sent\n");

    // 从服务器接收数据
    read( sock , buffer, 1024);
    printf("Server: %s\n",buffer);

    return 0;
}

段错误,核心已转储

什么是段错误

段错误(Segmentation Fault)是一种常见的程序错误,通常在访问无效的内存地址时发生。

可能产生段错误的情况

内存访问错误

无效的指令或操作

动态内存分配问题

栈溢出

库或依赖项问题

核心转储是转储到哪儿

当程序发生段错误时,操作系统会生成一个名为 core 或 core.<进程ID> 的核心转储文件,其中包含了程序崩溃时的内存映像和其他相关信息。这个core文件通常会被转储到当前工作目录下。

环境ubuntu

char* a = "123456";

scanf("%s",a);

报错,段错误

原因是a指向了常量区。需要用malloc动态开辟空间。

TCP常见粘包等问题思路,UDP没有粘包现象,为何

cpp 复制代码
//send(socketClientCfd, "Login", strlen("Login"), 0);	
send(socketClientCfd, "Login", strlen("Login")+1, 0);	
sprintf(buffer,"%d",userPhoneLogin);
send(socketClientCfd,buffer,1024,0);//发送手机号

这里的两个send在接收时出现了粘包,原因是第一个send直接使用字符串常量,在strlen("login")处没有给结束符\0留位置

牢记send和recv的字节数应严格相同,如果发送端发送字节数>接收端接收字节数,可能导致数据被拆分,如果发送端发送字节数<接收端接收字节数,可能导致接收端缓冲区存在数据残留,影响下一次读取

TCP/UDP

TCP怎么保证可靠性

  1. 序列号、确认应答、超时重传

连接建立以后发送方发送数据,首次数据会携带ISN初始序列号,此后每次单方向上的序列号为上个序列号+上次报文在整个字节流中的偏移量,ACK确认号为接收到的报文序列号+1。如果发送迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方会在等待一定事件后进行重传。发送方等待确认应答的时间一般为2*RTT+一个偏差值

  1. 窗口控制与快速重传(重复确认应答)

TCP会利用滑动窗口来控制流量,避免发送数据过多接收方无法处理。滑动窗口机制允许接收方通知发送方当前可以接收的数据量,发送方根据这个窗口大小来控制发送的数据量。在一个窗口大小内,发送方发送数据后无需等待确认。不使用窗口控制,每一个没收到确认应答的数据都要重发。

使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会触发重传机制。

简述一下TCP建立连接和断开连接的过程

TCP建立连接和断开连接的过程

三次握手

1、Client首先发送同步报文段。将请求标志位SYN置1,同时随机产生一个序列号Seq_Client,并将该数据包发送给Server以请求连接,Client进入SYN_SENT状态

2、Server回复确认报文段,将SYN和ACK都置为1,应答号ack=Seq_Client+1,同时随机产生一个序列号Seq_Server,将该数据包发送给Client以回复请求,Server进入SYN_RECV状态

3、Client收到确认后,检查收到的报文应答号ack是否为Seq_Client+1,应答标志位ACK=1,正确的话就把ACK置1,回复一个应答号为Seq_Server+1的确认报文段。Server收到后检查应答号和应答标志位,正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手。

四次挥手

TCP连接是全双工通信,每个方向都必须要单独进行关闭。

1、Client发送FIN标志位=1的结束报文段,并停止发送数据,进入FIN_WAIT_1状态

2、Server发送一个确认报文段给客户端,进入CLOSE_WAIT状态。Client收到后进入FIN_WAIT_2状态。

3、Server发送一个结束报文段给客户端,进入LAST_ACK状态。

4、Client返回一个ACK报文,进入TIME_WAIT状态,等待2MSL(报文段往返时间)后关闭连接。

为什么是三次握手?

三次握手可以防止已失效的SYN连接请求报文段被送到服务端导致错误连接。一个例子就是客户端发送了一次连接请求,延迟传送到服务端,期间又发送了一次请求并成功连接,如果只两次握手服务端会认为客户端又传来了一个新的连接请求,并重新连接,导致服务器资源浪费。

还有就是客户端发送连接请求后就挂掉了,服务端这时建立连接也会浪费资源

为什么是四次挥手?

TCP协议是全双工通信,这意味着客户端和服务器都可以向彼此发送和接收数据,因此关闭连接是双方都需要确认的行为。

假设只有三次挥手,经历了客户端的FIN请求,服务端的应答,服务端的FIN请求,此时TCP连接处于半关闭状态(Half-Close)状态。如果服务端还有数据发送给客户端,客户端是接收不到的。

为什么等待2MSL后关闭连接?

服务器发送了FIN+ACK给客户端,客户端返回ACK后等2MSL后断开连接,是确保服务器能够收到最后一次应答报文段,因为服务器没收到的话会重发FIN+ACK重启第三次挥手。

TCP、UDP的特点是什么

TCP具有可靠、稳定的优点,三次握手四次挥手建立稳定的通信连接和释放流程,还有连接确认滑动窗口快速重传拥塞控制机制。缺点是效率低,维护连接消耗资源大。TCP适用于文件传输、邮件传输等场景。

UDP是无状态传输协议,数据传输快但是不稳定。适用于视频、语音等场景。

什么是TCP拥塞控制?达到什么情况开始减慢增长速度?

如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵。TCP为了防止这种情况进行了拥塞控制

TCP拥塞控制由4个核心算法组成。"慢启动"、"拥塞避免"、"快速重传"、"快速恢复"。

慢启动: 慢启动是TCP连接初始化时的一种算法,用于TCP连接时增加拥塞窗口的大小。拥塞窗口初始大小通常为1,每收到一个ACK报文段拥塞窗口的大小就*2,直到达到慢启动阈值
拥塞避免 :拥塞避免算法用于避免网络拥塞并保持网络的稳定性。一旦拥塞窗口大小达到慢启动阈值,发送方就会进入拥塞避免阶段,此时拥塞窗口的大小由收到一个ACK报文段*2改为+1,以此来避免拥塞。
快速重传:遇到3次序列号相同的ACK应答报文时,代表该序列号报文丢失,就会触发重传。
快速恢复 :当发送方触发快速重传后,会将拥塞窗口减半,然后进入快速恢复阶段。此时收到一个ACK报文段拥塞窗口大小增长恢复到*2,直到达到阈值。

拥塞窗口是同一时间服务器能处理的数据量的上限

OSI七层模型和TCP/IP协议栈四层模型的对应关系

OSI七层模型和TCP/IP四层模型的对应关系

OSI参考模型有7层,从上到下分别为:

(应用层、表示层、会话层)、运输层、网络层、(链路层、物理层)

应用层:在应用层规定数据格式 HTTP\FTP\DNS

表示层:负责数据的格式转换和加密 JPEG/ASCII

会话层:负责建立和管理会话连接 RPC/NFS

传输层:负责流量控制和错误检测 TCP/UDP

网络层:数据包的路由选择和转发 IP/ARP/ICMP

数据链路层:将字节组装成数据帧,MAC寻址 MAC/VLAN/PPP

物理层:数据比特流在物理条件下传输 IEE/CLOCK/RJ45

TCP/IP模型有4层,从上到下分别为:

应用层、运输层、网络层、网络接口层

应用层:包括各种应用协议如HTTP、FTP、SMTP等

传输层:负责端到端传输,TCP协议和UDP协议

网络层:负责主机之间通信,IP协议

网络接口层:定义数据在物理介质上传输的方式,以太网、WIFI等

TCP/IP数据链路层的交互过程是什么样的?

数据链路层使用MAC地址作为通信目标。数据包到达网络层准备往数据链路层发送的时候,首先会去ARP缓存表里查找ip对应的MAC地址,查到了就把IP对应的MAC地址封装到链路层数据包的包头;如果没有找到,就会发起广播到局域网中,请求具有特定IP地址的设备回复自己的MAC地址,一旦收到回复,就会更新ARP缓存表中的映射关系。

ARP,Address Resolution Protocol,地址解析协议

ARP缓存表中的每一项都包含以下信息:

- IP地址:设备的IP地址

- MAC地址:对应设备的MAC地址

- 接口:设备连接网络的接口

TCP/IP协议传递到IP层怎么知道报文该给哪个应用程序,它怎么区分UDP报文和TCP报文

IP层使用目标端口号确定报文的去向。IP协议头中的报文的标识字段可以确定是TCP报文还是UDP报文:UDP 17、TCP 6

浏览器输入URL到页面显示发生了什么

浏览器将URL解析为IP地址,涉及域名就要用到DNS协议。首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询包括递归查询和迭代查询。如果是迭代查询,本地的DNS服务器会向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。

得到IP地址后,浏览器与服务器建立一个HTTP连接。HTTP生成一个GET请求报文,将该报文交给TCP层处理,所以还会用到TCP协议。如果采用https还会先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,TCP数据包然后会发送给IP层,用到IP协议。IP层通过路由跳转到目标地址。在一个网段内的寻址是通过以太网协议实 现(当然也可以是其他物理层协议,如PPP,SLIP)以太网协议需要MAC地址,涉及ARP协议。

HTTPS和HTTP的区别

在http连接的基础上,HTTPS建立连接的过程会把自己的数字证书给客户端,客户端验证有效性后会生成一个随机的对称密钥用于加密后续的数据传输(TLS加密)。

HTTP是明文传输,HTTPS是经过TLS加密的

HTTPS在TCP三次握手以后,还需要进行SSL的握手

HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书

HTTP协议端口是80,HTTPS协议端口是443

HTTPS更安全,但是握手阶段延时高,且需购买CA证书和加解密,占用CPU资源多,部署成本高

SOCKET

请问你有没有基于SOCKET做过开发?具体网络层的操作该怎么做?

基于TCP的socket

服务端:socket-bind-listen-accept

客户端:socket-connect

结构体:addr和addr_in,用来绑定Ip地址和端口等属性

htons把16位本地字节序转网络字节序(大端),ltons把32位本地字节序转网络字节序(小端)

ntons把16位网络字节序转本地字节序

send/write和recv/read来做缓冲区的读写

close关闭文件描述符

基于UDP的socket

服务端:socket-bind-recvfrom-sendto

客户端:socket-sendto-recvfrom

close关闭文件描述符

客户端/服务端

URI(统一资源标识符)和URL(统一资源定位符)之间的区别

URL:

URL统一资源定位符(Uniform Resource Locator),其实就是"网址"

协议类型 : // 认证信息 @ 服务器地址 : 端口号 / 带层次的文件路径 ? 查询字符串 # 片段标识符

htttp : // user:pass @ www.example.jp : 80 / dir/index.html ? uid=1 # ch

URI:

URI统一资源标识符(Uniform Resorce Identifier),就是某个网络协议方案表示的资源的定位标识符。比如HTTP网址就是HTTP协议下的URI

URL是URI的子集。

为什么服务器易受SYN攻击?

SYN攻击是DDos攻击的一种,原理是伪造源地址 对服务器发送SYN请求,服务器需要回应SYN+ACK包,而真实地址由于并未发送连接请求不会对服务器做出回应,可能造成目标服务器中的半开连接队列被占满,从而阻止其他合法用户进行访问。SYN攻击数据包的特点是源地址发送大量SYN包,并缺少三次握手的最后一步握手ACK回复。

防护措施:

Syn Cache技术。SYN报文来了,不着急去分配资源处理连接,而是简单回复一个ACK报文,并在一个HASH表中缓存这种半开连接,直到收到正确的连接再分配资源处理。

Syn Cookie技术。Syn Cookie使用特殊的算法生成Seq序列,这种算法考虑到了对方的IP、端口、己方IP、端口和数据段长度、传输时间等信息,在收到对方的报文后会计算这些数据是否对应,从而决定是否分配资源

Syn Proxy防火墙。对到来的SYN请求进行验证以后才放行。

请问Server端监听端口,但还没有客户端

请问Server端监听端口,但还没有客户端连接进来,此时进程处于什么状态?

编程模型用的是阻塞式IO还是非阻塞式IO,如果是阻塞式IO就是阻塞状态,如果是非阻塞式IO那种IO复用的情况就是运行状态

数字证书是什么,里面包含哪些内容?

数字证书由认证中心(CA)颁发。根证书是认证中心与用户建立信任的基础。认证中心是一种管理机构,用户将自己的私用密钥对和公共密钥对传送给认证中心,认证中心核实身份后给用户颁发一个数字证书。在用户使用数字证书之前必须下载和安装。

请你说一下GET和POST的区别

get参数通过url传递,post放在request body中

get请求在url中的参数有长度限制,而post在request body中的参数无限制

get参数直接暴露在url中,相比post不安全

get请求和post请求本质上就是tcp连接,但get请求直接把请求头和请求参数放一起打包成一个数据包,而post请求会把请求头和请求体分两个数据包传送。

C语言多线程和进程

c语言主线程不会等待子线程的完成,主线程结束子线程强制终止

c语言使用多线程需要引入pthread库,属于第三方库,GCC编译时也要加上-lpthread

<pthread.h>

多线程相关函数

//创建一个线程 成功返回0 失败返回错误代码

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数1:pthread_t类型指针变量

参数2:线程属性,通常为NULL

参数3:线程入口函数指针

参数4:线程入口函数的参数指针 只能一个参数,多个参数要结构体实现
//终止线程 并返回参数 正确终止线程需要调用pthread_exit(xx)或return来正确释放资源

void pthread_exit(void *retval);

参数:线程的返回值
//等待指定的线程终止,并获取该线程的返回值

int pthread_join(pthread_t thread, void **retval);

参数1:等待的线程

参数2:用于存储被等待线程返回值的指针

cpp 复制代码
/*主线程创建了一个子线程,并在子线程中循环打印消息,
然后调用`pthread_exit()`函数退出线程并返回一个字符串作为退出状态*/
void *thread_function(void *arg) {
    int i;
    for (i = 0; i < 5; i++) {
        printf("Thread is running\n");
        sleep(1);
    }
    pthread_exit("Thread finished execution");
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_function, NULL);
    void *thread_result;
    pthread_join(tid, &thread_result);//join等待线程结束并获取返回值
    printf("Thread result: %s\n", (char *)thread_result);
    return 0;
}

//获取线程ID

pthread_t pthread_self (void);
//分离指定线程,子线程结束时自动回收资源 成功返回0

int pthread_detach(pthread_t thread);

线程同步

实现线程同步的常用方法有4种,分别称为互斥锁、信号量、条件变量和读写锁

互斥锁

互斥锁的初始化

POSIX 标准规定,用 pthread_mutex_t 类型的变量来表示一个互斥锁,该类型以结构体的形式定义在<pthread.h>头文件中。

初始化pthread_mutex_t变量的方式有两种,分别为:

cpp 复制代码
//1、使用特定的宏
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//2、调用初始化的函数
pthread_mutex_t myMutex;
pthread_mutex_init(&myMutex , NULL);
互斥锁的"加锁"和"解锁"
cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t* mutex);   //实现加锁 阻塞,成功返回0
int pthread_mutex_trylock(pthread_mutex_t* mutex);  //实现加锁 不阻塞,返回EBUSY错误代码
int pthread_mutex_unlock(pthread_mutex_t* mutex);   //实现解锁 成功返回0 返回错误码
cpp 复制代码
//mutex
pthread_mutex_t mutex;
//购买商品
void *buy(void *arg){
    pthread_mutex_lock(&mutex);
    if(num>0){
        printf("%ld:购买成功\n",pthread_self()%10000);
        num--;
    }else{
        printf("%ld:购买失败,库存不足。\n",pthread_self()%10000);
    }
    pthread_mutex_unlock(&mutex);
}
//main
int main(){
    //初始化锁
    pthread_mutex_init(&mutex,NULL);
    //开启t1,t2两线程执行buy操作
    pthread_t t1;
    pthread_create(&t1,NULL,buy,NULL);
    pthread_t t2;
    pthread_create(&t2,NULL,buy,NULL);
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
}

线程间通信

cpp 复制代码
//互斥量
pthread_mutex_t mutex;
//条件变量
pthread_cond_t cond;
//等待并释放当前互斥锁。必须在持有互斥锁的情况下使用,否则会导致未定义的行为
pthread_cond_wait(&cond, &mutex);
cpp 复制代码
/*经典面试题 三线程打印A、B、C,要求输出ABCABC
    思路,一个标注位,线程判断是不是自己的标志位,不是就wait,是就打印了唤醒下一个线程*/
pthread_mutex_t mut;
pthread_cond_t ca;
pthread_cond_t cb;
pthread_cond_t cc;

void *printA(){    
    while(1){
        pthread_mutex_lock(&mut);
        if(flag!='A'){
            pthread_cond_wait(&ca,&mut);
        }
        printf("A\n");
        flag='B';
        pthread_cond_signal(&cb);
        sleep(1);
        pthread_mutex_unlock(&mut);
    }
}
void *printB(){
    while(1){
        pthread_mutex_lock(&mut);
        if(flag!='B'){
            pthread_cond_wait(&cb,&mut);
        }
        printf("B\n");
        flag='C';
        pthread_cond_signal(&cc);
        sleep(1);
        pthread_mutex_unlock(&mut);
    }
}
void *printC(){
    while(1){
        pthread_mutex_lock(&mut);
        if(flag!='C'){
            pthread_cond_wait(&cc,&mut);
        }
        printf("C\n");
        flag='A';
        pthread_cond_signal(&ca);
        sleep(1);
        pthread_mutex_unlock(&mut);
    }
}
int main(){
    pthread_t ta;
    pthread_t tb;
    pthread_t tc;
    pthread_create(&ta,NULL,printA,NULL);
    pthread_create(&tb,NULL,printB,NULL);
    pthread_create(&tc,NULL,printC,NULL);
    pthread_join(ta,NULL);
    return 0;
}

进程

进程相关函数

fork() 创建子进程,复制fork以下所有代码到子进程进行

cpp 复制代码
int main(){
    int pid=fork();
    printf("hello\n");
    return 0;
}
//加了Fork()以后打印处两句hello 一句是主进程打印,一句是子进程打印

getpid() 获取当前进程号

cpp 复制代码
int main(){
    int pid=fork();
    printf("%d:fork return %d\n",getpid(),pid);
    return 0;
}
//主进程跑fork返回子进程的id,子进程跑fork返回0
// 7729:fork return 0
// 7728:fork return 7729

僵尸进程

当主进程尚未结束,子进程已结束,这时子进程会成为僵尸进程,标志Z。

僵尸进程,我们需要用wait()/waitpid()去销毁。

cpp 复制代码
int main(){
    int pid=fork();
    if(pid!=0){
        printf("father %d start\n",getpid());
        sleep(30);//此期间可观察子进程为Z状态
        wait(NULL);//销毁子进程[同步执行][阻塞式执行]
        //waitpid(pid,NULL,WNOHANG);销毁子进程[异步执行][非阻塞式执行]
        sleep(30);//此期间可观察子进程消失,父进程活着。
    }else{
        printf("son %d complete\n",getpid());        
    }
    return 0;
}

信号/信号处理

//信号处理函数 接收到指定信号时,会执行相应的处理函数

void (*signal(int signum, void (*handler)(int)))(int);

参数1:指定信号,如SIGINT信号,由Ctrl+C触发

参数2:信号处理函数,收到触发信号后执行

cpp 复制代码
#include <unistd.h>
#include <signal.h>
void handle(){
    printf("触发系统中断\n");
    exit(0);
}
int main(){
    //为SIGINT信号,注册自定义处理函数。
    signal(SIGINT,handle);
    //常规死循环
    while(1){
        printf("hello\n");
        sleep(1); }
    return 0;
}
//程序可以捕获 `SIGINT` 信号,并执行一些清理操作后再退出,以确保程序能够优雅地终止

几种常见信号

SIGINT - 系统中断信号

SIGALRM - 闹钟信号

SIGCHLD - 子进程结束信号

cpp 复制代码
void handle(){
    printf("触发闹钟\n");
    exit(0);
}
int main(){
    //给SIGALRM信号注册处理函数
    signal(SIGALRM,handle);
    printf("开闹钟,延时10秒触发。\n");
    alarm(10);//延时发出闹钟信号
    sleep(30000);
    return 0;
}

用SIGCHLD更好的把控僵尸进程的销毁时机

cpp 复制代码
include <stdio.h>
#include <sys/types.h> 
#include <unistd.h>
#include <sys/wait.h>
void handle(){
    printf("子进程结束了,待销毁。\n");
    wait(NULL);
    printf("子进程销毁ok!\n");
}
int main(){
    //给SIGCHLD信号绑定处理函数handle
    signal(SIGCHLD,handle);
    int pid=fork();
    if(pid!=0){
        printf("father %d start\n",getpid());
        sleep(60);
    }else{
        printf("son %d complete\n",getpid());
        sleep(10);//10秒后子进程执行结束,发出SIGCHLD信号,立刻会拦截到handle函数处理。
    }
    return 0;
}

进程间通信

无名管道(PIPE)

无名管道用于有亲缘关系的两个进程之间的通信

管道是半双工的,即数据只能在一个方向上流动。如果需要双向通信,通常需要创建两个管道

/*管道(Pipe)是一种特殊的文件,用于实现进程间通信,

其中一个进程的输出直接作为另一个进程的输入*/

int pipe(int filedes[2]);

参数:整型数组,用于存储两个文件描述符,

`filedes[0]` 用于读取数据,`filedes[1]` 用于写入数据

无名管道通信案例

cpp 复制代码
int main(){
    int fd[2];
    pipe(fd);//创建管道。fd[0]为读端,fd[1]为写端。
    int pid=fork();
    //父进程代码区
    if(pid!=0){
        close(fd[0]);//关闭读端
        write(fd[1],"hello",10);//向管道写入hello
        printf("%d:send:hello\n",getpid());
    }
    //子进程代码区
    else{
        close(fd[1]);//关闭写端
        char buf[10];
        read(fd[0],buf,10);//从管道读取
        printf("%d:read:%s\n",getpid(),buf);
    }
    return 0;
}

有名管道(FIFO)

int mkfifo(const char *pathname, mode_t mode);

参数1:指定要创建的命名管道的路径和名称

参数2:指定权限模式,用于设置文件的权限

cpp 复制代码
//创建有名管道
mkfifo(path,0777);
//打开管道,获取文件描述符,用于向管道写入或从管道读取。
int fd=open(path,O_RDWR);
read(fd, buffer, sizeof(buffer));
close(fd);

消息队列

/*在使用 System V 消息队列、共享内存和信号量时,

通常需要通过 `ftok()` 来生成一个唯一的键,以便标识这些 IPC 对象。*/

key_t ftok(const char *pathname, int proj_id);

参数1:路径名

参数2:IPC(进程间通信)对象ID
//如果指定的键值对应的消息队列已经存在,则返回该消息队列的标识符;

//否则创建新的并返回消息队列的标识符

int msgget(key_t key, int msgflg);

IPC对象的Key_t键值

消息队列的权限标志
/*向 System V 消息队列发送消息*/

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

消息队列标识符

消息结构体指针

消息大小/字节

消息标志位

IPC_NOWAIT 当消息队列已满时,`msgsnd()` 将立即返回,而不是等待消息队列变得可用。这样可以避免阻塞进程。

MSG_NOERROR 当消息太大而无法完全放入消息队列时,系统将截断消息,而不是返回错误

MSG_EXCEPT 发送消息时,如果队列中已有消息,该标志位用于发送消息给队列中的第一个进程,而不是最后一个进程

MSG_COPY 发送消息时不使用共享内存
//从System V 消息队列接收消息

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

消息队列标识符

接收消息缓冲结构体指针

接收消息缓冲区大小

接收消息类型标识符,为0表示接收队列中第一条消息

接收消息标志位

cpp 复制代码
//生产方
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//消息结构体
typedef struct MyBuf{
    long type;
    char text[200];
} MyBuf;
int main(){
    //create Q 创建队列
    key_t key=ftok("./Q",'A');            //创建IPC对象的Key_t键
    int qid=msgget(key,0777|IPC_CREAT);   //为键值创建消息队列并返回
    while(1){
        printf("input msg:\n");
        //mybuf 创建预发送的消息
        MyBuf mybuf;
        mybuf.type=1;
        scanf("%s",mybuf.text);
        //send to Q 把消息发送到队列
        msgsnd(qid,&mybuf,20,0);          //向指定的消息队列发送内容
        printf("send success!\n");
    }
    return 0;
}
cpp 复制代码
//消费方
//消息结构体
typedef struct MyBuf{
    long type;
    char text[200];
} MyBuf;
int main(){
    //创建队列
    key_t key=ftok("./Q",'A');
    int qid=msgget(key,0777|IPC_CREAT);
    //从队列接收消息
    MyBuf mybuf;
    msgrcv(qid,&mybuf,20,1,0);
    printf("recv:%s\n",mybuf.text);
    return 0;
}

讲一下基于消息队列实现进程间通信的流程

基于消息队列实现进程间通信是一种常见的IPC方式,不同进程之间通过消息队列来传递数据。

引入<sys/ipc.h>和<sys/msg.h>

通过ftok来创建消息队列的键值

通过msgget来获取消息队列的标识符

通过msgsnd来向标识符对应的消息队列发送结构体缓冲

通过msgrcv来从标识符对应的消息队列读取内容

相关推荐
szuzhan.gy9 分钟前
DS查找—二叉树平衡因子
数据结构·c++·算法
火云洞红孩儿33 分钟前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
青い月の魔女1 小时前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
最后一个bug2 小时前
STM32MP1linux根文件系统目录作用
linux·c语言·arm开发·单片机·嵌入式硬件
FeboReigns2 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns2 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
zh路西法2 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt2 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小2 小时前
C++面试八股文:指针与引用的区别
c++·面试
沐泽Mu3 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式