嵌入式八股文-C/C++基础(速记版)

C/C++基础

C和C++有什么区别

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

        C++ 的函数return一个对象的时候,实际上会调用拷贝构造函数或者移动构造函数。

        而C语言没有对象的概念,也不支持返回复合类型,只能返回基本类型或者符合类型的指针。

基本数据类型

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

整型:

  • int : 有符号整数类型,32位。  

短整型:

  • short int:有符号短整数类型,16/32位。//简写为short

长整形:

  • long int:有符号长整数类型,32/64位。//简写为long。一般显式添加后缀 L。否则当int类型

无符号型:

        unsigned int:32位。

        unsigned short:无符号短整数类型,16/32位。

        unsigned long:无符号长整数类型,32/64位。 

字符型:

  • char:有符号字符类型,8位。

无符号字符型:

  • unsigned char:无符号字符类型,8位。

浮点型:

  • float:单精度浮点数类型,32位。有效位6/7。
  • double:双精度浮点数类型,64位。有效位15/16。
  • long double:长双精度浮点数类型,64位。有效位18/19。

布尔型:

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

        C99 标准引入了 stdint.h 头文件,它定义了一系列具有明确大小和符号性的整数类型,如 int32_t、int64_t、uint32_t、uint64_t等。这些类型提供了跨平台的整数大小一致性。

        要查看编译器对基本类型的取值范围,查看编译器目录/limits.h。

main函数的参数有什么作用

        main函数一般两个参数,第一个参数指的是字符串个数,第二个参数是一个二维数组argv,argv[0] 用来存放main文件路径字符串

关键字

Volatile有什么作用

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

  XBYTE[2]=0x55;
  XBYTE[2]=0x56;
  XBYTE[2]=0x57;
  XBYTE[2]=0x58;

编译器会对上述四条语句进行优化,忽略前三条语句,只产生一条机器代码。如果键入 volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)

Static关键字

Static有什么作用

1.定义变量

表示变量的作用域仅限于本文件函数外面不能使用该变量,仅程序启动时被初始化一次

注意extern 和 static 的含义是互相矛盾的,不会同时用

2.  定义函数

静态函数只能在本源文件中使用;也就是说在其他源文件中可以定义和自己名字一样的函数

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

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

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

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

静态成员函数是类的一部分,而不是对象的一部分,对象的静态成员数据共享对象的静态存储空间,类的静态成员函数不能对类中的非静态成员进行访问,即不能在静态函数里面使用this指针

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

其实所有变量初始化都只有一次但是Static变量保存在全局区不被销毁,而auto变量,即自动变量,由于它存放在栈区,一旦函数调用结束,就会立刻被销毁。

结构体 Struct 和联合体 Union

联合体union所有成员共用一块地址空间

对联合体的不同成员赋值,将会导致对其他成员的重写

union要求占用空间能容纳最大成员空间,

要求总大小为对齐标准的整数倍,union的首地址也要能被对齐标准整除

struct结构体不同成员会存放在不同的地址,

以最宽成员的对齐字节作为对齐标准

要求每个成员变量的起始地址能被自己的类型对齐

要求总大小为对齐标准的整数倍

char

int

char 

short

char

char 1字节,占0x00,填充到0x03

int 4 字节,占0x04~0x07                //每个成员变量首地址要能被自己的类型对齐

char 1字节,占0x08,填充到0x19  

short 2字节,占0x10~0x11,

char 1字节,占0x12        ,此时一共13字节,填充到16,满足总大小被对齐标准对齐
#pragma pack(1) 修改字节对齐量为1

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

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

#pragram(1)   //设置字节对齐  (按1字节对齐)
Struct card{
        int a;
        char b[20];
        double c;}
#pragram()   //开启字节对齐

/*注意,C语言字节对齐是
#pragma(x)
……
#pragma*/

/*注意,C++语言字节对齐是
#pragma pack(x)
……
#pragma pack()*/
Struct结构体在C和C++中有什么区别

1. C语言中结构体不允许有函数,C++可以

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

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

4. 在C中不可以初始化结构体数据成员,C++可以

5.  空结构体在C++中占1个字节,在C中占0字节。

        C语言结构体不允许有函数,但可以有函数指针。 

        空结构体在C++占1个字节,因为C++标准规定任何对象不能有相同的地址,如果给0字节的话多个空结构体必然有相同地址出现。 

使用Union判断大小端问题

定义一个union联合体,里面放一个short放一个char[2],

然后给short赋值,判断1是在高字节还是低字节

大小端转换如何实现

利用按位与、按位或和移位操作,从低字节开始0xff这样按位与然后移位

Const常量

Const有什么作用?

用来定义常量,可以修饰变量、指针、函数

如果定义全局的Const 常量,会放在全局区的数据段

如果定义局部的Const常量,会放在栈区,生命周期跟函数一样

C语言内存模型包括 代码段(含常量区)、数据段、BSS段、堆、栈、映射区

C语言内存结构

什么情况下使用const关键字?

const关键字比较特殊的就是修饰指针时有 指针常量常量指针 的区别

1	const  int*p;  //常量指针,指向常量的指针。即p指向的内存可以变,p指向的数值内容不可变
2	int const*p; //同上
3	int*const p;//指针常量,本质是一个常量,而用指针修饰它。 即p指向的内存不可以变,但是p内存位置的数值可以变
4	const int* const p;//指向常量的常量指针。即p指向的内存和数值都不可变

         在另一连接文件中引用 const常量。使用方式有

1	extern const int 1:
2	extern const int j=10;

        此处未特别提及的,const关键字和类型修饰符前后顺序替换无区别

C语言 Volatile 能不能修饰 Const ?

volatile可以修饰const,const不是真的定义成常量,初始化仍然是在程序启动后而不是编译期。volatile只是说让变量直接去数据总线取值,不经过缓存。

Enum枚举

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

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

Typedef

#define 和 typedef的区别

#define 是宏定义,属于预处理命令,在预处理只做字符串替换,不作正确性检查

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

#define 和 typedef 定义指针的区别
#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)(); //定义一个函数指针,指向返回类型为int的函数

Extern链接

Extern 的作用是什么

Extern用来声明外部变量

Extern "C" 的作用是什么

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

Register变量

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

默认局部变量Auto

局部变量默认都是auto类型,存储在栈中

左值和右值是什么?

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

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

说说右值和右值引用吧

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

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

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

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

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

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

**右值引用** 是一种引用类型,用 `&&` 表示,它可以绑定到右值。

移动语义指的是将资源所有权从一个对象转移到另一个对象,而不是进行昂贵的深层复制

//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 上申请时需要指定申请的内存大小,返回值需要强制类型转换。

malloc底层原理

1)当开辟的空间小于 128K 时,系统调用 brk()函数,移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)

2)当开辟的空间大于 128K 时mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

        brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。 

        当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。其实就是之前操作出现的地址连续的空闲内存在逻辑上合并成空闲的整块内存。

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

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

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

#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.(##)符号连接操作符

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

#define exampleNum( n ) num##n

使用:

int num9 = 9;
int num = exampleNum( 9 ); //将会扩展成 int num = num9

注意:

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

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

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

sizeof关键字和strlen函数

        strlen函数返回字符串的长度,不包含'\0'

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

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

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

strlen("hello") - sizeof("hello") = 5 - 6 

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结束

字符串拷贝

char *strcpy(char *dest, const char *src);

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

int strcmp(const char *s1, const char *s2);

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

字符串拼接 

char *strcat(char *dest, const char *src);

//俩字符串拼接成一个新的

字符复制 

void *memset(void *s, int c, size_t n);

//复制字符 到参数 str 所指向的字符串的前 n 个字符

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

从字符串复制前N个字节到目标字符串

void *memcpy(void *dest, const void *src, size_t n);

//复制前N个字节到目标字符串前N个

字符串转整数 

int atoi(const char *nptr);

//字符数组转整数

求数据大小

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

Int *a;
Sizeof(a);  //结果为4

求引用大小 

Sizeof(char &)  //4  引用就是别名,没有自己的地址,底层是指针常量,4字节

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

两个变量的指针强转成char*,然后相减

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

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

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

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

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

1. gets可以接收空格,scanf遇到空格结束(scanf对于xxx xxx有空格会导致缓冲区残留)

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

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

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

gets和scanf都会在读到的末尾添加\0     scanf会把结束符\n留在缓冲区中

printf()函数的返回值

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

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

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

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

scanf( " %c ",&ch);会导致缓冲区存在\n残留

可以使用while(getchar()!='\n');清缓冲区或者直接使用

fflush(stdin);清缓冲区

C语言随机数

    // 设置种子 通常情况下,可以 srand(time(0)) 来使用当前时间作为种子
    // 使用 rand() 生成随机数,通过取余限制随机数的范围

内存

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

1. 静态存储区分配

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

2. 栈上分配

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

3. 堆上分配

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

堆与栈有什么区别

1. 空间申请方式

        栈空间由操作系统分配/释放,堆空间需要手动分配/释放

2. 空间申请大小的限制

        堆栈的大小是提前设置好的,如果溢出就会提示overflow。

        栈地址向低地址扩张,是一块连续的区域。

        堆地址向高地址扩张,是不连续的区域,系统使用链表存储空闲的堆内存地址。

3. 空间申请效率

        栈由于频繁调用,使用一级缓存, 通常调用完毕立即释放;

        堆由于调用不频繁,使用二级缓存,速度要慢些。

        计算机硬件领域是三级缓存。 

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

1、函数整个运行期间都是在栈中处理,函数的返回地址、参数、临时变量等都保存在栈中。

2、栈是操作系统多线程管理的基石,每个线程都有专属的栈,中断和异常处理也有专属的栈。

栈的空间最大值是多少?

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

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);

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

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

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

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

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

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

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()的唯一指针值。

2. realloc(void *ptr,size_t size)  //重新申请内存

        判断原来地址的连续空间容量够不够,够就直接扩容,不够就给个新地址。

3. calloc(memsize,size)  //申请 n 块 size 大小的内存

        申请内存空间后,会自动初始化内存空间为 0

内存泄漏

什么是内存泄漏

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

如何判断内存泄漏

1. new/delete、malloc/free 配套使用

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

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

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

指针、数组、引用

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

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

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

/*  int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
    int (*p)[4]; //p是一个数组指针,它指向一个包含有4个int类型数组的指针
    p = b;
    printf("%d\n", **(++p); //程序输出结果为 5  */

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

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

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

1. 函数指针

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

int (*p)(int , int); 

2. 指针函数

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

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

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

1. 概念不同
        数组:是同种类型的集合
        指针:里面保存的地址的值
2.赋值:
        
同种类型指针之间可以直接赋值数组只能一个个元素赋值
3.修改内容不同
        
数组可以基于下标修改内容
        指针赋值字符串是指向字符串常量,常量不允许修改

4.所占字节不同

        在 32 位系统中指针固定占 4 个字节数组大小要看元素类型和个数

5. 使用环境

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

        数组多用于线性表且内存由系统隐式分配。

  char p[ ] =”hello”, p[0] = ‘s’可以执行原因是 p 是数组可以基于下标修改内容
  char * p = “hello” 执行 p[0] = ‘s’ 报错,因为 p 指针赋值字符串是指向字符串常量,常量不允许修改

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

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

如果将指针强转为普通类型,则变成数值加法

对于二级指针++,由于二级指针指向指针,指针大小固定为4字节,故运算后内容未知

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

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

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

(ulong)p+0x200 = 0x10000000 + 0x200 = 0x10000200    经过ulong后变成数值加法了

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

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

1. 指针常量

int * const p;

先看const 再看*,p是一个常量类型的指针指向的地址不能改,地址的值可以改

2. 常量指针

1	const int *p;
2	int const *p;

先看 *再看 const,定义一个指针指向一个常量地址可以改,地址上的值不能改。 

3. 指向常量的常量指针

 const int *const p;

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

指针的运算

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

*的优先级高于++,谁离变量近优先处理谁,括号优先级最高

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

野指针是指指向未知地址的指针,产生原因通常是释放内存后,指针没有及时置空。

        如何避免野指针:

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

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

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

        4.  使用智能指针

int *p1 = NULL;                     //初始化置 NULL
p1 = (int *)calloc(n, sizeof(int)); //申请 n 个 int 内存空间同时初始化为 0
assert(p1 != NULL);                 //判空,防错设计
free(p1);
p1 = NULL;                          //释放后置空

什么是断言Assert ( int expression )

        void assert( int expression );

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

        通常情况下,程序调试完成之后会禁用所有的assert()

1、在源代码中禁用

包含在<assert.h>之前定义NDEBUG宏,就可以禁用所有的assert():

#define NDEBUG

2、在编译时禁用

在编译命令行中定义NDEBUG宏。例如,使用GCC编译器时:

-gcc -DNDEBUG file.c

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

int rows = sizeof(arr) / sizeof(arr[0]);
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);

指针作为参数的注意点

典型易错点

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

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

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

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

数组名 num/&num 的区别

对于一维数组来说,num+1 是偏移到下个元素,&num+1是偏移整个数组

对于二维数组来说,num+1 是偏移到下个一维数组,&num+1 是偏移整个数组

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

相同

1. 指针的内容是所指内存的地址引用是内存的别名,本质上是指针常量

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

区别

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

2. 指针++是对内存地址自增,而引用++是对值的自增

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

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

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

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

转换

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

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

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

二级指针和指针引用

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;     //一级指针指向的地址

数组的运算

以下代码表示什么意思?

设有二维数组 a[][]

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

 *(a[1]+1)

对于二维指针 **a 来讲,记住行列值,a运算是行,*a运算是列,**a运算是值

这里队列做加法再取值,得到的就是 2行2列的值

*(&a[1][1])

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

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

 (*(a+1))[1]

a+1是行+1,其实就是二维数组指向的地址加一,解引用得到指向一维数组的指针,再通过下标取值,也就是第二行第二列的值

数组下标可以为负数吗?

可以,因为下标是与当前地址相对的偏移量数组定义不能为负数,但访问可以用负数。

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

局部变量每次初始化有可能被分配到同一地址,如果不使用动态分配,极大可能每次给局部变量分配相同地址导致问题

同时,C语言中二维数组地址空间也是连续的

智能指针

什么是智能指针

智能指针是一个类,用来存储指针。C++11引入了智能指针的概念方便管理内存。包括:

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

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

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

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

弱引用智能指针 weak_ptr 的构造函数不会指向引用计数的共享内存但可以检测到所管理的对象是否已经被释放,从而避免非法访问

位操作

题目:求解整型数的二进制表示中1的个数?

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

当x!=0时,清除了几次就有几个1

1	int func(int x)  //功能函数: 求解二进制中1的而个数
2	{
3	    int countx = 0;
4	    while(x)         //当不为 0 的时候
5	    {
6	        countx++;    //计数
7	        x = x&(x-1); //把 x 对应的二进制数中的最后一位 1 去掉
8	    }
9	return countx;
10  }

其实直接算0的个数直接&1判断结果是不是0,然后右移就行,但是效率低

题目:如何求解整型二进制中 0 的个数

&1判断结果是不是0,然后右移就行,效率低

x|(x+1),将最低位的0变成1,while(x+1)计数,由于int x,x最大时补码正数变最小负数,退出循环

预处理

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

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

下面举个例子:

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

1	#ifdef XXX
2	...
3	#else
4	#endif

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

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是预处理命令,只做简单的字符串替换不具备安全性检查。同时宏定义不具有作用域限制,只要是前面任何地方定义过的宏定义,后续都可以使用。

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

define 和 typedef 修饰指针效果不同

1 #define INTPTR1 int*
2 typedef int* INTPTR2;
3 INTPTR1 pl, p2;
4 INTPTR2 p3, p4;

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

#define SECOND_PER_YEAR (60*60*24*365)UL

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

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

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

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

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

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

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

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

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  }

变量

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

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

局部变量是在函数内定义的变量

全局变量静态变量(全局静态变量、局部静态变量)都分配在数据段

局部非静态变量分配在上。

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

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

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

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

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

函数

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

每个函数都有属于自己的栈帧结构。

栈帧结构由两个指针指定,帧指针指向起始,存放着上一个栈帧的头部地址栈指针指向栈顶,也就是函数返回地址,函数对大多数数据的访问都是基于帧指针

函数是代码的一部分,编译时被转换成机器码,函数声明(函数签名)可以在多个地方被引用

栈帧只处理局部变量,全局变量、静态变量数据段处理。

在函数调用时,通常的顺序是:

- 调用函数时,将当前函数的帧指针压入栈中(保存上一个函数的帧指针)。

- 将栈指针移动以为新的栈帧分配空间

- 将帧指针移动新栈帧底部

- 执行函数体内的操作。

- 函数返回时,将栈指针和帧指针恢复到上一个函数的状态

栈帧结构示意图

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

通常使用 EBP寄存器 作为帧指针ESP寄存器作为栈指针

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

//Extended Base Pointer,EBP寄存器

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

可以使用GNU项目的GCC编译环境,GCC编译器特性提供了关键字支持声明函数时指定在main之前还是main之后执行

GNU项目(UNIX平台)对函数属性的主要设置关键字如下:

        alias:   设置函数别名

        aligned:设置函数对齐方式

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

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

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

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

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

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

//keil默认的是ARMCC编译 

示例代码如下:

#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

为什么可能作为父类的类,其析构函数必须是虚函数?

虚函数的设计说白了就是为了支持动态绑定

new一个子类时,用基类指针指向子类对象,虚函数的虚表指针可以通过查虚函数表,使得delete的时候正确释放子类空间

每个具有虚函数的类的具体实现都有一个虚表指针指向该类的虚函数表,表的元素是虚函数指针。当调用虚函数时,查找对象的虚函数表找到正确的派生类函数。

不管继承类有没有覆盖父类的虚函数,虚函数都按照其声明顺序放于表中,没覆盖父类的虚函数在子类的虚函数前面,覆盖了的顺序排列

派生类的虚函数地址依照声明顺序放在第一个基类的虚表最后,多继承也按照继承顺序排列虚函数指针

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

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

静态函数和虚函数的区别

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

重载和重写(覆盖)

重写

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

重载

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

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

     每个具有虚函数的对象实例都有一个指针指向虚函数表,虚函数表按照函数声明顺序,将函数地址存在虚函数表里子类对象重写的父类的虚函数也会按顺序存放在虚函数表里,虚函数在编译时开辟空间,运行时动态绑定。基类指针指向子类对象,调用子类函数时,虚函数表可以指明调用的具体函数是哪一个。

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

无参构造重载构造拷贝构造移动构造

无参构造重载构造,在定义类的对象时,完成对象的初始化工作

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

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

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

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

拷贝构造参数是常量引用const A & a

移动构造函数,主要是依靠右值引用在函数调用中解决亡值(临时对象)带来的效率问题

移动构造由于要修改参数指向临时变量空间,因此参数不用const修饰

假设有一个类A,在函数中创建A的局部对象并return出去,其实并不会直接返回A的值

C++11之前

是先创建一个临时变量拷贝构造函数把a拷贝到临时变量,然后a析构,临时变量拷贝到对象b,然后临时对象析构。这种情况对临时变量做了两次拷贝,影响效率。

C++11之后,是a拷贝到临时变量,然后b直接右值引用临时变量,临时变量不回收。

A func()   //定义func()函数
{
    A a;   //创建对象a
    return a;   //返回对象a
}
int main()
{ 
    A b = func();   //调用func()函数
    return 0;
}

/* 结果 */
构造函数       ---a的
拷贝构造函数   ---a的
析构函数      ----a的
----main结束后----
析构函数       ---b的
/* 注意此处只调用了一次拷贝构造函数,是因为VS编译器参考C++11标准做了优化,减少了临时对象的生成 */
C++11前
C++11后

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

编译器会自动生成 拷贝构造函数无参构造函数

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

无参构造函数、拷贝构造函数、析构函数

赋值运算符有赋值运算符重载函数

前置++和后置++也有各自的重载函数

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

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

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

Select函数

作用:监听设置的 fd 集合

工作流程:

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

1. select函数原型

/*
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);

2. 文件描述符的数量

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

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

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

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

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

5. select的触发方式

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

6.  优缺点

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

文件描述符集合操作

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

#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 , &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,错误返回 -1exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回 -1

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` 是一个模板参数包,表示零个或多个附加的模板参数。这种语法通常用于定义接受可变数量参数的模板,允许在模板实例化时传入不固定数量的参数。

字符输入函数

英文一个字符占1个字节,ascii码,汉字一个字符占3个字节,utf-8

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

定义处理信号的函数

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

#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>

    //打开指定文件,若不存在则创建。
    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函数读取文件时,文件指针不会改变,仍然指向之前的位置。

    //打开指定文件,若不存在则创建。
    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字节位置处

//打开指定文件,若不存在则创建。
    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符不保留)

 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 对象指定了一个输出流。

    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输入流
    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 对象字符流

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关键字 定义

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。 
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大象荒野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值