C语言查漏补缺

429 阅读20分钟

数据类型

image-20220904170517872

符号常量:

define PI = 3.14 //宏定义

进制

int i;

未初始化时,在为i分配的4个字节中填充的是0xcccccccc,如下所示:

image-20220908162906181

计算器中选择DWORD,表示四个字节。

执行i = 2;内存变化成为0x02000000

image-20220908163014178

这里采用的是小端存储(X86架构皆如此),即低字节放在前面,且字节内部不反转,故第一个字节为02

image-20221026142505824

上图中一行对应16个地址,每个地址对应一个字节,一个字节8位。

浮点数

浮点数表示(用e表示10的幂次):

image-20220908170533913

做算法题时遇到的一个关于double的问题:

double类型的 2.4 可能由 2.33333333表示,所以取整数时需要对其加上1e-8

混合运算

整型算除法需要向上取整

标准输入

输入时,只有换行后才会进行真正的IO操作,否则,只是将输入缓存在输入缓冲区。

如果输入是char类型,会读入诸如'\n',空格等字符;否则将忽略这些字符(含字符串)。

#include <stdio.h>

int main() {
    int i, j;
    char s[3];
    char a; 
    scanf("%s%d",&s, &j);
    printf("%s", s);
    //这里不会继续读取,而是直接拿到缓冲区的'\n'输出
    scanf("%c", &a);
    printf("%c", a);
    return 0;
}

循环输入:

//输入返回值是读入的字符
while((ref = scanf("%d",&a)) != EOF);
//上述代码如果给的输入是一个字符,不是整型,就可能一直循环,所以可以在每次读取前将缓冲区进行清空rewind/fflush
while(rewind(stdin), (ref = scanf("%d",&a)) != EOF);

如果和字符进行混合输入,要在%c之前加上空格,否则读入诸如8 c 2.1这样格式的输入时会出错。因为空格会被当成一个字符进行读入。

scanf("%d %c%f",&a, &c, &b);

另外,int型可以接受char的输入,但是数值不是对应的ASCII码,原因可能是将ASCII存入四字节中出问题。

但是char类型接受的字符输入可以强制转换为正确的ASCII码。

#include <stdio.h>
int main(){
    int c, d;
    float b;
    scanf("%d %c %f",&d,&c, &b);
    printf("%d",c);	//-858993567
    printf("%c",c);	//a
}

换行输入时可能会存在\n未被消化的问题,可以自己使用scanf("%c",&c);这样的语句进行消化。

下面的代码中使用到了gets(),该函数读到\n停止,如果没有对上面输入的\n进行消化,那么直接读到\n,就停下了,后面的字符输入无法进行获取。

image-20221027145204368

运算符与表达式

C语言中的运算符:

image-20221026101909270

关系运算符之间的优先级问题:

优先级运算符结合律
1后缀运算符:[] () · -> ++ --(类型名称){列表}从左到右
2一元运算符:++ -- ! ~ + - * & sizeof_Alignof从右到左
3类型转换运算符:(类型名称)从右到左
4乘除法运算符:* / %从左到右
5加减法运算符:+ -从左到右
6移位运算符:<< >>从左到右
7关系运算符:< <= > >=从左到右
8相等运算符:== !=从左到右
9位运算符 AND:&从左到右
10位运算符 XOR:^从左到右
11位运算符 OR:|从左到右
12逻辑运算符 AND:&&从左到右
13逻辑运算符 OR:||从左到右
14条件运算符:?:从右到左
15赋值运算符: = += -= *= /= %= &= ^= |= <<= >>=从右到左
16逗号运算符:,从左到右

sizeof被认为是一元运算符而非函数。

int a[10];
sizeof(a); //返回40
void test(int a[10]){
    sizeof(a); //返回4,参数传递时并不会传递数组长度,数组名中存的是数组的首地址,即指针
}

C语言认为一切非0值都是真。

一个对浮点数进行判等的问题:

float f = 234.56;
if(f == 234.56) //false,存在精度问题
//选用0.0001是因为小数的有效位数问题
if(f - 234.56 > -0.0001 && f - 234.56 < 0.0001) //true

逻辑运算符组成的式子叫做逻辑表达式,对应的值只有真和假。

数组

数组长度定义时,当前标准可以使用变量,但是最好还是写成常量

注意数组越界问题

字符串

“CHINA”在内存中占有6个字节:

image-20220909124823396

另外 ,('\0' == 0 ) is true

char c[2] = {'c','v'};
printf("%s",c);	//输出cv烫烫烫╔  以为输出字符串时只有遇到\0才会停下,'\0'的ASCII值对应为0
//判断字符串是否结束就可以对应ASCII是否为0
int str[] = "test";
while(str[i]) putchar(str[i++]);

注:字符串要有'\0'结束符

char c[] = "asd";
printf("%d",sizeof(c)); //输出4

读取字符串时,字符串中包含空格的解决方案:

char c[10] = "";
gets(c); 	
fgets(c, sizeof(c), stdin); //fgets也可以,但是会将最后的\n也读入,使用c这个字符串时可以将最后的\n替换成\0
字符串相关函数

下面的函数中接受类型为const,不是代表需要传入const的字符串,而是指传入该函数中的字符串是不可修改的。

image-20221026170448768
char c[] = "asd";
char str[6];
printf("%d",strlen(c));
strcpy(str, c);
printf("\n%s", str);
printf("\n%d", strcmp(c,str)) ;
printf("\n%d", strncmp(c,str,5)) ;	//只比较五个字符
//拼接时注意数组不要越界,返回值时拼接后字符串的首地址,strcpy与之相同
char *test = strcat(str,"s");
putchar('\n');
puts(str);

注意:strcmp函数 如果第一个比第二个大,返回1,小则返回-1,相等返回0。比较的是对应位置的ASCII,都相等的话比长度。

C语言字符串函数补充:

//字符串中查找字符
printf("\n%d", strncmp(c,str,5)) ;	//只比较五个字符
char *string = "Hello World!";
char *result = strchr(string, 'l');	//从头开始找字符	llo World!
char *result_reverse = strrchr(string, 'l');	//倒着开始找字符	ld!

//字符串中查找字符(字符可以是多个)
//查询多个匹配的字符,下面的例子种“;”和“,”皆可为查找对象
char *string2 = "C, 1972; C++, 1983; Java, 1995; Rust, 2010; Kotlin, 2011";
char *break_set = ",;";

int count = 0;
char *p = string2;

do {
    p = strpbrk(p, break_set);
    if (p) {
        puts(p);
        p++;
        count++;
    }
} while (p);

PRINTLNF("Found %d characters.", count);

//查询字串
char *substring_position = strstr(string,"Wor");
puts(substring_position);
PRINT_INT(substring_position - string);

//分割字符串
char *next = strtok(string, field_break);	//根据field_break获取其前的字符串,分割之后会把分割符置为NULL
next = strtok(NULL, language_break);	//如果string传入NULL就一直分隔原先的字符串,状态维护

判断字符类型
#include <io_utils.h>
#include <ctype.h>

int IsDigit(char c) {
  return c >= '0' && c <= '9';
}

int main() {
  //返回0是假,非0为1 
  PRINT_INT(isdigit('0'));
  PRINT_INT(isspace(' '));
  PRINT_INT(isalpha('a'));
  PRINT_INT(isalnum('f'));	//字符表和数字
  PRINT_INT(isalnum('1'));
  PRINT_INT(ispunct(','));	//标点符号
  return 0;
}
和内存操作相关的几个函数
//memcopy对比strcpy记忆
char a[] = "aaa";
char c[10];
char *p = "dsada";
//memcpy(p,a,sizeof(a));  //会出错,需要先分配空间,p只是指向字符串首的一个指针(字符串常量池)
p = c;  //让p等于已经分配空间的字符数组
memcpy(p,a,sizeof(a));
puts(p);

//memchar 和 strchr对比
char a[] = "aaa";
//多了一个maxcount参数,表示最多找几个,返回找到的第一个指针
char *find = memchr(a,'a',3);
puts(find);

//memcmp对比strcmp,多了一个参数指代比较位数
memcmp(a, p, 3);

//memset
char c[10];
memset(c, 'h', sizeof c);	//给C数组中全部赋值为h
c[9] = 0;
puts(c);

//memmove和memcopy效果一致,但是当内存有重叠时最好使用memcopy
char a[10] = "aasdsa";
memmove(&a[3],&a[1],sizeof(a));
puts(a);

指针

内存区域中的每字节都对应一个编号,这个编号就是“地址”,如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元.按变量地址存取变量值的方式称为“直接访问”,如 printf("%d",i);、scanf(" %d",&i);等;另一种存取变量值的方式称为“间接访问”,即将变量i的地址存放到另一个变量中.在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。

image-20221026191303899

image-20221026191345158
char p = 'p';
char *pointer = &p;
printf("%d\n",sizeof pointer);	//输出4,写的程序是win32控制台程序,64位输出是8
//只读指针变量
int *const cp = &a;
*cp = 2;	//自己可以改,别人不能改
cp = &b; //ERROR 不能修改指针指向

//只读变量指针
int const *cp = &a;
*cp = 2;	//ERROR
cp = &b; //OK

//指向之路类型变量的只读指针
int const *const cp = &a;
*cp = 2;  //ERROR
cp = &b; //ERROR 不能修改指针指向

取地址操作符为&,也称引用,通过该操作符我们可以获取一个变量的地址值;取值操作符为*,也称解引用,通过该操作符我们可以得到一个地址对应的数据。

如图所示,我们通过&i获取整型变量i的地址值,然后对整型指针变量p进行初始化, p中存储的是整型变量i的地址值,所以通过第12行的*p就可以获取整型变量i的值.P中存储的是一个绝对地址值,那为什么取值时会获取4字节大小的空间呢?这是因为p为整型变量指针,每个int型数据占用4字节大小的空间,所以p在解引用时会访问4字节大小的空间,同时以整型值对内存进行解析.

image-20221026193230076

那么&* pointer_1的含义是什么呢? “&”和“*”两个运算符的优先级别相同,但要按自右向左的方向结合.因此,&* pointer_1与&a相同,都表示变量a的地址,也就是pointer_1。 *&a的含义是什么呢?

首先进行&a运算,得到a的地址,再进行运算。&a和*pointer_1的作用是一样的,它们都等价于变量a,即*&a 与a等价。

//引用值传递
void change(int *j){
    *j = 2;
}
int main(){
   int i = 3;
    change(&i);
    printf("%d",i);
}

指针的偏移

偏移量取决于一个指针变量所占的空间大小。

数组名 a 类型是数组,a里存了一个值,是数组首地址值。数组名可以理解为制度只读指针int *const arr_p = arr;

定义指针最好进行初始化,否则可能编译不通过。

image-20221027112640328
//指针自增
int a[3] = {2,7,8};
int *p;
int j;
p = a;
j = *p++;	//是对p进行加加,*本身也是一个运算符
printf("a[0] = %d, j = %d, *p = %d\n",a[0], j, *p);
//输出:a[0] = 2, j = 2, *p = 7
j = (*p)++;	//相当于p[0]++  ==> [] 的优先级是高于 ++的
printf("a[0] = %d, j = %d, *p = %d\n",a[0], j, *p);
//输出:a[0] = 3, j = 2, *p = 3

为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢?

这是由于一维数组名中存储的是数组的首地址。如数组名c中存储的地址为0x0015faf4,所以子函数change 中其实传入了一个地址(传递时数组弱化为指针)。定义一个指针变量时,指针变量的类型要和数组的数据类型保持一致,通过取值操作,就可将“h”改为“H”,这种方法称为指针法。获取数组元素时也可以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法。

image-20221027115258605

动态内存申请

C语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间。

程序是放在磁盘上的有序的指令集合。程序启动起来时才叫进程。

运行时用到内存空间,内存又分为栈空间和堆空间。

image-20221027120829639

一开始已经知道的空间使用栈存储,动态分配的使用堆空间,申请的堆空间需要自己进行释放。栈空间的效率是要高于堆空间的。

int i;
scanf("%d",&i);
char *p;
//malloc申请空间后,返回的时所申请空间的首地址
//这个指针是无类型指针void*,需要进行强转,强制转换也能运行,但是会警告,初试也可能扣分
p = (char*)malloc(i);
//需要知道啊realloc的用法
char *t = realloc(p, i * 2);	//分配两倍的空间,如果返回指针为NULL,则表示内存分配失败
//把字符串内容拷贝进刚申请的空间
strcpy(p, "hahhaha");
puts(p);
//释放空间,p所指的地址没有变化,但分配的空间被回收了
free(p);
//在free p之后要将p置为NULL,因为此时p为野指针,容易出错
p = NULL;

:如果分配空间返回指针为NULL,则表示内存分配失败

栈空间和堆空间的差异

栈打印:

char* printfstack(){
    char c[10] = "sadasd";
    puts(c);	//输出sadasd
    return c;
}
int main(){
    puts(printfstack());	//输出╔
}

栈空间会随着函数执行结束而释放,所以main函数中再去访问c指向的位置时已经被释放了。

堆空间打印:

char* printfstack(){
    char* c = (char*) malloc(10);
    c = "sadasdad";
    puts(c);	//输出sadasdad
    return c;
}
int main(){
    puts(printfstack());	//输出sadasdad
}

堆空间不会随着函数执行结束而释放。

栈空间内的数值是随机的,而堆空间会初始化为0。

全局变量、static变量在堆空间中。

指针字符串的问题

在下面的代码中char* p = "hello"使p指向的时字符串常量池中的数据(内存除了堆栈还有数据区以及字符串常量区),所以不能进行修改;

而c的地址是字符串在方法栈中的地址,可以进行修改。

但是在Clion编译器中则可以编译执行通过,原因未可知。

image-20221027153445660
char *p = "hello";	//最好不要指针指向字符串后改变其值
char *t = "hello";
char c[10] = "hello";
c[0] = 'H';
p[0] = 'H';  
putchar(c[0]);
putchar(p[0]);
p = "test";	//成功赋值
//c = "test"; 报错
puts(p);
return 0;

补充:对于内存的三种操作是 读、写、执行

二级指针

一级指针的使用场景是传递与偏移,服务的对象是整型变量、浮点型变量、字符型变量等。二级指针也是一种指针,其作用自然也是传递与偏移,其服务对象更加简单,即只服务于一级指针的传递与偏移。(二级指针的偏移考研无需掌握)

void change(int **p, int *pj){
    //p(&pi)中存的是pi的地址,这里让其等于pj的地址
    //解引用后改变其里的内容
    *p = pj;
}
int main(){
    int i = 10;
    int j = 5;
    int* pi;
    int* pj;
    pi = &i;
    pj = &j;
    printf("%d %d %d %d\n",i, j, *pi, *pj);
    //对指针变量取地址
    change(&pi, pj);
    // let *pi equals to 5
    printf("%d %d %d %d",i, j, *pi, *pj);
}

函数

新建func.h以及func.c,新建的多个c文件中只能有一个main函数,引入自定义的头文件使用双引号

在Clion中的目录结构以及配置:

image-20221027165738326
//####func.h####
#include <stdio.h>
void printstar(int i);
//####func.c####
#include "func.h"

void printstar(int i){
    while(i--) putchar('*');
    putchar('\n');
}
//####main.c####
#include "func.h"
int main(){
    printstar(1);
}

一个C程序由一个或多个程序模块组成,每个程序模块作为一个源程序文件.对于较大的程序,通常将程序内容分别放在若干源文件中,再由若干源程序文件组成一个C程序。这样处理便于分别编写、分别编译,进而提高调试效率.一个源程序文件可以为多个C程序共用。

命令行编译多个文件(让c文件和头文件在同一目录下):

gcc main.c func.c -o main

另外,C语言中没有重载的概念。

  • 执行过程

先编译,func.c => func.obj;main.c => main.obj

再链接,func.obj + main.obj => main.exe

隐式声明:C语言中有几种声明的类型名可以省略。例如,函数如果不显式地声明返回值的类型,那么它默认返回整型。

使用旧风格声明函数的形式参数时,如果省略参数的类型那么编译器默认它们为整型。然而,依赖隐式声明并不是好的习惯,因为隐式声明容易让代码的读者产生疑问:编写者是否是有意遗漏了类型名?还是不小心忘记了?显式声明能够清楚地表达意图。

从用户角度来看,函数分为如下两种。 (1) 标准函数:即库函数,这是由系统提供的,用户不必自己定义的函数,可以直接使用它们,如 printf函数、scanf函数。不同的C系统提供的库函数的数量和功能会有一些不同,但许多基本的函数是相同的。 (2) 用户自己定义的函数:用以解决用户的专门需要。

函数之间的通信可以使用全局变量,全局变量存储在内存的数据区中。

image-20221027180152124

可以在函数中定义和全局变量同名的变量,不冲突,变量使用遵循就近原则,可以使用extern关键字访问外部变量。如下图,大括号中也是局部变量,大括号内可用。

image-20221027194844524

使用print(ij,i++)是不合适的,因为C标准未规定函数调用是从左到右计算还是从右到左计算,因此不同的编译会有不同的标准,造成代码在移植过程中发生非预期错误。

结构体

有时候需要将不同类型的数据组合为一个整体,以便于引用.例如,一名学生有学号、姓名、性别、年龄、地址等属性,如果针对学生的学号、姓名、年龄等都单独定义一个变量,那么在有多名学生时,变量就难以分清。为此,C语言提供结构体来管理不同类型的数据组合。

补充关于字符串的一点:

image-20221028112429127

由上图不难发现,&a 和 a是一样的,都指向&a[0],所以scanf("%s",&a);scanf("%s",a);均合法。但一般是不写&的。

如下代码中结构体变量s的内存格局:

内存中0a 00 00 00部分对应代码中的char sex;,虽然char类型只有一个字节,但是为了读取方便,提高CPU访问内存的效率,需要进行对齐操作,所以占了四个字节。最后的addr也进行了对齐,加上前面的sex一共增加了5个多余字节。

​ e9 03 00 00 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
6e cc cc cc 0a 00 00 00 00 00 c8 42 68 65 6e 61
6e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 cc cc

注:结构体的大小不能人工计算,要sizeof

#include "func.h"
struct student{
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};
int main(){
    struct student s = {1001,"test",'n',10,100,"henan"};
    struct student sarr[3];
    for(int i = 0; i < 3; ++i){
        scanf("%d%s %c%d%f%s",&sarr[i].num, sarr[i].name, &sarr[i].sex, &sarr[i].age, 									&sarr[i].score,sarr[i].addr);
    }
}

结构体指针

struct student s = {1001,"test",'n',10,100,"henan"};
struct student* p;
p = &s;
//当输出结构体中的某个属性时,由于“.”运算符的优先级高于“*”,所以需要加小括号
printf("%s\n", (*p).name);
//或者直接使用“->”运算符可对指针选择成员
printf("%s", p -> name);
int num = p -> num++;
printf("%d %d\n",num, p->num);  //1001 1002 , ++ 和 -> 属于同级运算符,从左向右结合,所以是p->num进行了++
num = p++ -> num;
printf("%d %d",num, p->num);    //1002 -858993460 , ++ 和 -> 属于同级运算符,从左向右结合,所以是指针p进行了++
//上述例子中p的指针进行了偏移,所以指向位置为乱码,如果是数组指针,则会指向下一个下标

数组结构体可以以这种方式赋值:

image-20221028161223895

typedef

typedef的作用简要来说就是起别名。起别名是为了代码即注释。

//如果不加typedef的话表示定义结构体的同时创建变量,不建议定义的时候创建变量
typedef struct student{
    int num;
    char name[20];
    char sex;
}stu, *pstu;
int main(){
    stu s = {1001,"haha",'N'};
    puts(s.name);
    pstu sp = &s;
    puts(sp -> name);
}

C++引用

#include <iostream>
#include "stdlib.h"
using namespace std;

void modifynum(int &b){
    b += 1;
}
void modifypointer(int *&b){
    //这里对引用指针的操作和主函数一致
    b = (int*) malloc(sizeof(int));
    b[0] = 1;
}
int main() {
    int a = 10;
    modifynum(a);
    cout << a << endl;
    int *p = NULL;
    modifypointer(p);
    cout << p[0];
    return 0;
}

C++字符串差异

char str[100];
cin >> str;             // 输入字符串时,遇到空格或者回车就会停止
cout << str << endl;    // 输出字符串时,遇到空格或者回车不会停止,遇到'\0'时停止
//带空格读入一行
fgets(str, 100, stdin);  // gets函数在新版C++中被移除了,因为不安全。
// 可以用fgets代替,但注意fgets不会删除行末的回车字符
//也可以使用下面这种方式
cin.getline(str,100);

//如果是string类型进行读入
string s;
getline(cin, s);

C++中的String

//头文件记得引入
#include <iostream>
#include "cstdio"
#include "string"

string s1 = "sadasds";
//开辟副本而非同引用
string s2 = s1;
s1 = "sad";
cout << s1 << endl;
cout << s2;


//做加法运算时,字面值和字符都会被转化成string对象,因此直接相加就是将这些字面值串联起来
//当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string

string s4 = s1 + ", ";  // 正确:把一个string对象和有一个字面值相加
//string s5 = "hello" + ", "; // 错误:两个运算对象都不是string

string s6 = s1 + ", " + "world";  // 正确,每个加法运算都有一个运算符是string
//string s7 = "hello" + ", " + s2;  // 错误:不能把字面值直接相加,运算是从左到右进行的

string s = "hello world";

//处理string对象中的字符
for (int i = 0; i < s.size(); i ++ )
    cout << s[i] << endl;
for (char c: s) cout << c << endl;
//使用&符号的修改才会生效
for (char& c: s) c = 'a';

cout << s << endl;

//字符串转数字,这是c语言中的方法
cout << atoi("123") << endl;

string s1, s2 = "abc";
//为空是true
cout << s1.empty() << endl;
cout << s2.empty() << endl;
cout << (s2.size() >= 2 && s2.size() <= -1) << endl;    //无符号数 true s2.size() <= -1恒成立

C++结构体、类差异

类的定义:

#include <iostream>

using namespace std;

const int N = 1000010;

class Person
{
    //私有,只能在类内进行访问
    private:
        int age, height;
        double money;
        string books[100];
	//共有
    public:
        string name;

        void say()
        {
            cout << "I'm " << name << endl;
        }

        int set_age(int a)
        {
            age = a;
        }

        int get_age()
        {
            return age;
        }

        void add_money(double x)
        {
            money += x;
        }
    //可以定义多个
    private:
        int test;
} person_a, person_b, persons[100];

int main()
{
    //声明变量
    Person c;

    c.name = "szy";      // 正确!访问公有变量
    c.age = 18;          // 错误!访问私有变量
    c.set_age(18);       // 正确!set_age()是共有成员变量
    c.add_money(100);

    c.say();
    cout << c.get_age() << endl;

    return 0;
}

结构体(使用方式同类):

struct Person
{
    private:
        int age, height;
        double money;
        string books[100];

    public:
        string name;

        void say()
        {
            cout << "I'm " << name << endl;
        }

        int set_age(int a)
        {
            age = a;
        }

        int get_age()
        {
            return age;
        }

        void add_money(double x)
        {
            money += x;
        }
} person_a, person_b, persons[100];

类和结构体的构造函数:

#include <iostream>
using namespace std;
const int N = 1000010;
struct Person
{
    int age, height;
    double money;
    Person(int _age, int _height){age = _age; height = _height;}
    //精简写法
    Person(int _age) : age(_age){}
    Person();
} ;

int main()
{
    //声明变量
    Person c(1,2);
    cout << c.height;
    return 0;
}

链表:

#include <iostream>

using namespace std;

struct Node
{
    int val;
    Node* next;
    Node(int _val) : val(_val), next(NULL){}
    Node(){}
} *head;

int main()
{
    //new Node返回的是地址
    Node * tp = new Node();
    //不加new返回的是值
    Node t = Node();

    for (int i = 1; i <= 5; i ++ )
    {
        Node* p = new Node(i);
        p->next = head;
        head = p;
    }

    for (Node* p = head; p; p = p->next)
        cout << p->val << ' ';
    cout << endl;

    return 0;
}

C++ STL容器

Vector

vector是变长数组,支持随机访问,不支持在任意位置 O(1)插入。为了保证效率,元素的增删一般应该在末尾进行。

size函数返回vector的实际长度(包含的元素个数),empty函数返回一个bool类型,表明vector是否为空。二者的时间复杂度都是 O(1)。 所有的STL容器都支持这两个方法,含义也相同,之后我们就不再重复给出。

clear函数把vector清空。

迭代器就像STL容器的“指针”,可以用星号*操作符解除引用。

vector的迭代器是“随机访问迭代器”,可以把vector的迭代器与一个整数相加减,其行为和指针的移动类似。可以把vector的两个迭代器相减,其结果也和指针相减类似,得到两个迭代器对应下标之间的距离。

begin函数返回指向vector中第一个元素的迭代器。例如a是一个非空的vector,则*a.begin()a[0]的作用相同。

所有的容器都可以视作一个**“前闭后开”**的结构,end函数返回vector的尾部,即第n 个元素再往后的“边界”。*a.end()a[n]都是越界访问,其中n = a.size()

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

int main(){
    vector<int> a({1,2,3});
    vector<int> b[233]; //相当于第一维长度233,第二维数组长度为边长的二维数组
    struct Test{
        int a;
    };
    vector<Test> t; //类型为Test的变长数组
    //长度
    cout << a.size() << endl;
    //是否为空
    cout << a.empty() << endl;
    //清空元素
    //a.clear();
    //迭代器
    vector<int>::iterator it = a.begin();
    //a的最后一个位置的下一个位置 *a.end 和 a[n] 都属于越界访问
    a.end();
    //两者等价
    *a.begin() == a[0];

    //两种遍历元素的方法
    for (int i = 0; i < a.size(); i ++)
        cout << a[i] << endl;

    for (vector<int>::iterator it = a.begin(); it != a.end(); it ++)
        cout << *it << endl;
    //可以使用auto进行类型推断
    for (auto it = a.begin(); it != a.end(); it ++)
        cout << *it << endl;
}

front函数返回vector的第一个元素,等价于*a.begin()a[0]。 back函数返回vector的最后一个元素,等价于*--a.end()a[a.size() – 1]

a.push_back(x)把元素x插入到vector a的尾部。 b.pop_back()删除vector a的最后一个元素。

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

int main(){
    vector<char> c = {'1', '2', '3'};
    cout << c[0] << " " << c.front() <<  " " << *c.begin() << endl;
    cout << c[c.size() - 1] << " " << c.back();
    //删除最后一个元素
    c.pop_back();
    //在最后添加一个元素
    c.push_back('4');
    return  0;
}

Queue

队、优先队列、栈都没有clear 函数。

头文件queue主要包括循环队列queue和优先队列priority_queue两个容器。

#include <iostream>
#include <queue>
#include "algorithm"
#include <vector>
using namespace std;

int main(){
    queue<int> q;
    q.push(1);  //插入一个元素
    q.push(2);
    q.pop();    //弹出元素
    q.front() ; //返回队头
    int i = q.back();   //返回队尾
    cout << q.front() << " " << i << endl;
    //返回队列大小
    cout << q.size() << endl;
    //队列、优先队列和栈都没有clear函数,清空的话,初始化即可
    q = queue<int>();

    //默认大根堆
    priority_queue<int> a;
    //小根堆
    priority_queue<int, vector<int>, greater<int>> b;

    priority_queue<pair<int, int>> c;
    struct Test{
        int t, a;
        //如果是自定义的struct,要重载比较符号
        bool operator< (const Test& t) const{
            return a < t.a;
        }
        bool operator> (const Test& t) const{
            return a > t.a;
        }
    };
    //大根堆要重载小于号
    priority_queue<Test> d;
    //小根堆要重载大于号
    priority_queue<Test, vector<Test>, greater<Test>> e;
    e.push({1,2});
    e.push({3,2});
    e.push({2,1});
    //取出小根堆最小值
    Test tt = e.top();
    cout << tt.a << endl;

    vector<Test> t = {{1,2},{3,1},{2,1}};
    //逆序排序
    sort(t.rbegin(),t.rend());
    for(int i = 0; i < 3; i++) cout << t[i].a << " " << t[i].t << endl;
}

Stack

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

int main(){
    stack<int> s;
    s.push(1);
    s.push(2);
    cout << s.top();
    s.pop();
}

Deque 双端队列

双端队列deque是一个支持在两端高效插入或删除元素的连续线性存储空间。它就像是vector和queue的结合。与vector相比,deque在头部增删元素仅需要 O(1) 的时间;与queue相比,deque像数组一样支持随机访问。

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

int main(){
    deque<int> deque;
    deque.push_back(1);
    deque.push_front(2);
    //返回头尾迭代器
    deque.begin();
    deque.end();
    deque.front();
    deque.back();
    //支持下标访问
    deque[0];
    //支持前后弹出
    deque.pop_back();
    deque.pop_front();
    //支持清空
    deque.clear();
}

Set

头文件set主要包括set和multiset两个容器,分别是“有序集合”和“有序多重集合”,即前者的元素不能重复,而后者可以包含若干个相等的元素。set和multiset的内部实现是一棵红黑树,它们支持的函数基本相同。

注意:s.lower_bound(x)查找大于等于x的元素中最小的一个,并返回指向该元素的迭代器。

s.upper_bound(x)查找大于x的元素中最小的一个,并返回指向该元素的迭代器。

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

int main(){
    set<int> s({3,1,2,4});   //元素不能重复
    multiset<int> ms;  //元素可以重复
    //常用的三个函数
    s.size();   s.empty();    s.clear();
    //迭代器
    set<int>::iterator iterator = s.begin();
    //表示在有序序列中的下一个
    iterator++;     //-- 表示在有序序列中的前一个元素
    cout << *iterator <<endl;   //2

    s.insert(6);
    s.find(2);  //返回值等于2的迭代器
    cout <<  (s.find(7) == s.end()) << endl;    //如果找不到会等于a.end(),可以判断是否存在

    s.lower_bound(3);   //找到大于等于3最小的元素的迭代器
    s.upper_bound(2);   //找到大于2最大的元素的迭代器

    s.erase(2); //把所有等于2的元素删除
    s.erase(iterator);  //把迭代器指向的元素删除

    s.count(2); //计算值为2的元素的个数,set返回1或者0,multiset可能返回 >1 的数

    struct Test{
        int a, t;
        bool operator< (const Test& t) const{
            return a < t.a;
        }
    };
    set<Test> ts;   //结构体要重载小于号
}

Map

#include <iostream>
#include <map>
#include <vector>
using namespace std;

int main(){
    //一半不使用
   multimap<string, int> mp;
   map<string, int> m;
   //两种插入方式,支持和数组一样操作
   m["asd"] = 1;
   m["dsa"] = 2;
   m.insert({"sd",2});
}

其他容器

#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <bitset>
using namespace std;

int main(){
    //无序的容器无法二分

    //无序set 底层实现是hash表
    unordered_set<int> a;   //无序,不可重复
    unordered_multiset<int> b;  //无序,可重复

    //无需map
    unordered_map<int, int> m;
    unordered_multimap<int, int> mm;

    //定义一个长度为1000位的01串
    bitset<10> bs, as;
    bs[0] = 1;
    cout << bs[0] << " " << bs[1] << endl;      //1 0,未被赋值的皆为0
    cout << bs.count() <<endl; //返回1的个数
    bs = 3;
    cout << bs << endl; //输出0000000011
    bs.set(3);  //第三位设为1
    bs.reset(3); //第三位设为0
    //或运算
    bs |= as;

    //pair二元组
    pair<int, string> p;
    //p = {1, "sad"};
    p = make_pair(1, "asd");
    pair<int, string> p1 = {2,"sad"};
    //输出一个二元组
    cout << p.first << p.second << endl;
    //比较二元组 ,先比较第一个再比较第二个
    cout << (p > p1) << endl;
    //注意:Vector的比较也已经实现了,两个数组的元素依次比较
}

C++位运算与库函数

位运算

image-20221030112058045

常用操作:

  1. 求x的第k位数字 x >> k & 1
  2. lowbit(x) = x & -x,返回x的最后一位1

第二个常用操作的证明:

1010110010000 a

0101001101111 取反~

0101001110000 +1 => -a

a & -a => 10000

reverse

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main(){
    vector<int> v1 = {1,2,3,4};
    //反转vector
    reverse(v1.begin(), v1.end());
    for(auto v : v1) cout << v << " " ;
    cout << endl;
    //反转数组
    int a1[5] = {1,2,3,4,5};
    reverse(a1, a1 + 5);
    for(auto a : a1) cout << a << " ";
    cout << endl;
}

unique

unique 要保证相同元素挨着。

返回去重(只去掉相邻的相同元素)之后的尾迭代器(或指针),仍然为前闭后开,即这个迭代器是去重之后末尾元素的下一个位置。该函数常用于离散化,利用迭代器(或指针)的减法,可计算出去重后的元素个数。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main(){
    vector<int> v1 = {1,2,2,3,3,4};
    //不同元素的数量
    //int m = unique(v1.begin(), v1.end())  - v1.begin();
    //cout << m << endl;
    //cout << endl;
    //去除去重后无用的数据,只留下有用的
    v1.erase(unique(v1.begin(), v1.end()), v1.end());   //输出1 2 3 4,由于去重要保证一样的数在一起,所以两次连续去重会出问题(上面的代码注释了)
    for (auto v : v1) cout << v << " ";
    cout << endl;

    //数组去重
    int a1[5] = {1,2,3,4,5};
    int n = unique(a1, a1 + 5) - a1;
    cout << n << endl;

}

random_shuffle

#include <iostream>
#include <algorithm>
#include <vector>
#include <ctime>
using namespace std;

int main(){
    vector<int> v1 = {1,2,2,3,3,4};
    //用时间戳做随机种子,使每次结果不一
    srand(time(0));
    //打乱顺序
    random_shuffle(v1.begin(), v1.end());
    for(auto v :v1) cout << v << " ";
    cout << endl;
}

sort

注:比较二元组数组时,先比较第一个再比较第二个

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

struct Test{
    int i, j;
} a[5];

bool cmp(int a, int b){
    //返回true则表示应该在前面,下面的返回值表示 a < b 时a在前,故而从小到大
    return a < b;
}
bool cmps(Test a, Test b){
    //从大da
    return a.i > b.i;
}
int main(){
    vector<int> v1 = {6,7,5,3,5,4};
    //从小到大
    sort(v1.begin(), v1.end());
    for(auto v : v1) cout << v << " ";
    cout << endl;
    //从大到小
    sort(v1.begin(), v1.end(), greater<int>());
    for(auto v : v1) cout << v << " ";
    cout << endl;

    //也可以自定义cmp
    sort(v1.begin(), v1.end(), cmp);
    for(auto v : v1) cout << v << " ";
    cout << endl;

    //结构体排序
    for (int i = 0; i < 5; ++i) {
        a[i].i = i; a[i].j = i + 1;
    }
    sort(a, a + 5, cmps);
    for(auto ta : a) cout << ta.i << " "  << ta.j << endl; ;

    //结构体排序重载比较符号
    struct Test2{
        int i, j;
        bool operator< (const Test2 &t) const{
            return i < t.i;
        }
        bool operator> (const Test2 &t) const{
            return i > t.i;
        }
    } t[5];
    for (int i = 0; i < 5; ++i) {
        t[i].i = i; t[i].j = i + 1;
    }
    //从小到大重载小于号
    sort(t, t + 5);
    //从大到小重载大于号
    sort(t, t + 5, greater<Test2>());
    for(auto tt : t) cout << tt.i << " " << tt.j << endl;
}

lower_bound & upper_bound 二分

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main(){
    vector<int> v1 = {6,7,5,3,3,5,4};
    sort(v1.begin(), v1.end());
    //找不到返回的是v1.end()
    cout << *lower_bound(v1.begin(), v1.end(), 3) << endl;  //大于等于(找到第一个3的zhi'z) -> 3
    cout << *upper_bound(v1.begin(), v1.end(), 3) << endl;  //严格大于 -> 4
}

补充:预处理和宏

image-20221029105722141

引入头文件就是为了引入使用到的外部函数和外部变量的声明。

image-20221029112654391