数据类型

基本类型

  • 整型int(4B=32b,可以表示2^32)

    long long(8B=64b,可以表示2^64个数)

    1
    2
    3
    4
    5
    6
    7
    #define _CRT_SECURE_NO_WARNINGS
    #include<stdio.h>
    int main() {
    int a, b;
    scanf("%d %d", &a,&b);
    printf("%d", a+b);
    }
  • 字符型char(1B=8b)

    用单引号''括起来的,只能包含一个字符

    1
    2
    3
    4
    5
    6
    7
    #include<stdio.h>
    int main() {
    char c = 'a';
    printf("%c", c);

    return 0;
    }
  • 浮点型

    • 单精度float(4B=32b)
    • 双精度double(8B=32b)
  • 字符串常量

    使用双引号""将字符串括起来。

    • 控制字符

      字符串里可以有3种控制字符,

      %d 十进制整数

      %f浮点数

      %c字符

      当然字符串的后面需要,来隔开参数

构造类型

  • 数组[]
  • 结构struct
  • 联合union
  • 枚举enum

指针类型

  • 指针*

空类型

  • void

混合运算

C是根据表达式中的数据类型来得到结果的类型

e.g.

因为5和2是整型,得到整形的2,再通过类型转换等到2.000000

1
2
3
float f = 5 / 2;
printf("%f", f);
2.000000

那么,我们怎么输出2.5呢?

直接让表达式中的一个数的数据类型变成浮点类型(强制转换)

1
2
3
float f = (float)5 / 2;
printf("%f", f);
2.500000

scanf的原理

  • 我们在终端输入时,当终端遇到\n时,才进行I/O操作,即将内容复制到内存中的缓存区中(包括\n)。
  • scanf()会不停对缓冲区检测。当内存中的缓冲区为空时,scanf()会对进程进行阻塞。

缓冲区不为空时,scanf()会通过给定的字符与缓冲区匹配,控制字符(%d,%f,%c…)与缓冲区转化,并通过&取走。

printf()正好与scanf()相反,通过控制字符(%d,%f,%c…),转换成字符串,并输出到屏幕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {

int i;
char c;
scanf("i=%d", &i);
printf("%d", i);
scanf("%c", &c);//读取'\n'
printf("%c",c);//输出'\n'

return 0;
}
i=3//输入
3//输出

%d %f %lf

清除缓冲区的非空字符(清除一开始的空白和\n)开始读,直到遇到空白和\n结束。所以\n会留在缓冲区。

这里%d与%d中间加不加空格都可以。如果没有空格,scanf中的%d跳过空格。如果有,匹配空格 。

1
2
3
4
5
6
7
8
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
int a, b;
scanf("%d%d", &a,&b);
printf("%d", a+b);
}

在输入中两个数要加空格

1
2
1 2//输入
3//输出

如果我这样,肯定是不行的。因为第2个%d读的是逗号,scanf并不会赋值给b,而是直接结束返回。b还是原来的未定义值。

1
2
1,2//输入
-858993459//输出

%c

scanf("%c", ch)它读取一个字符。关键是它不会清除空白,也不会清除\n

整形数在0-128直接,可以用%c输出

%s

清除缓冲区的非空字符(清除一开始的空白和\n),到空白和\n结束

1
2
3
4
5
6
int main() {
int a[5] = { 1,2,3,4,5 };
char c[20], e[20];
scanf("%s%s", c, e);
printf("%s---%s", c, e);
}

返回值

  • 出错时,返回EOF=1

    多次Ctrl+z结束while循环,使scanf出错

  • 匹配缓冲区失败,返回0

    E.g.当用%d遇到字符a时。

  • 匹配缓冲区成功变量的个数

scanf循环输入

循环输入整数

1
2
3
4
5
6
7
8
9
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
int i, ret;
while (rewind(stdin), (ret=scanf("%d",&i))!=EOF)
{
printf("i=%d\n", i);
}
}

循环输入字符

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
char c;
while (scanf("%c",&c))
{
if (c != '\n')
printf("%c", c);
else
printf("\n");
}
}

循环输入混合类型数据

  • 要点:在%c前加入一个空格
1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
int i;
char c;
float f;
while (scanf("%d %c%f",&i,&c,&f))
{
printf("%d %c %.2f\n", i, c, f);
}
}

运算符与表达式

  • 算术运算符(+, -, *, /, %)

    由算术运算符所组成的表达式,称算术表达式

    逆序输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #define _CRT_SECURE_NO_WARNINGS
    #include<stdio.h>
    int main() {
    int n;
    scanf("%d", &n);
    while (n!=0)
    {
    printf("%d", n % 10);
    n = n / 10;
    }
    return 0;
    }
  • 关系运算符(>, <, ==, >=, <=, !=)

    C语言认为一切非0值都是真,C语言没有布尔类型

    不要用关系运算符连写(1<a<8),是错误的

  • 逻辑运算符(!, &&, ||)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include<stdio.h>
    int main() {
    int a = 18;
    if (a > 2 && a < 10)
    printf("a is right");
    else
    printf("a is wrong");
    return 0;
    }
  • 赋值运算符(=)

    赋值运算符的左边只能放变量

  • 逗号运算符(,)

    逗号表达式的整体值是最后一个表达式的值

  • 自增自减运算符(++,–)

    注意:i++++i

    如果是i++那么,就让等到本语句运行完后,最后才将i=i+1,记住它是直接改变i的值

    如果是++i,直接按优先级运算即可。

    1
    2
    3
    4
    5
    6
    7
    8
    #include<stdio.h>
    int main() {
    int i = -1;
    int j;
    j = i++ > -1;//任何时候,先去掉 后++,等到本语句运行完后,再执行++
    printf("i=%d,j=%d", i, j);
    return 0;
    }
  • sizeof运算符

    打印变量的占据的字节数

循环

  • while
  • for

当需要运行指定数量的循环时,就用for。当需要某个条件使循环结束时就用while

for的执行循序

for(表达式1;表达式2;表达式3)语句;
for循环语句的执行过程如下
(1)先求解表达式1。
(2)求解表达式2,

​ 为真(值为非0),则先执行for语句中指定的内嵌语句,后执行 第(3)步。

​ 为假(值为0),则结束循环,转到第(5)步。

(3) 求解表达式3

(4) 转回第(2)步继续执行。
(5) 循环结束

数组

数组名[常量表达式]数组名是一个地址常量。

1
2
3
4
5
int main() {

char c[20] = "hello";
//c="world" //不能对地址常量c进行修改
}

数组具有以下特点:

  1. 相同数据类型
  2. 过程中需要保存数据
  3. 空间大小指定

访问越界:

访问越界会造成:Stack around the variable ‘a’ was corrupted.

1
2
3
4
5
6
#include<stdio.h>
int main() {
int a[5] = { 1,2,3,4,5 };
a[5] = 6; //访问越界
return 0;
}

传递整型数组

当我们传递数组时,不会把整个数组传递过去。只会传递数组的起始地址。

所以,我们传递数组时,还要传递数组长度

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
void print(int a[], int len)
{
int i;
for (i = 0; i < len; i++) {
printf("a[%d]=%d\n", i, a[i]);
}
}
int main() {
int a[5] = { 1,2,3,4,5 };
print(a, 5);
}

字符数组

我们用字符数组来存储字符串,字符串的结束标志为\0,所以,数组的长度要比字符串长度至少多1个字节。

如果字符数组长度大于字符串长度,会自动添加\0

1
2
char c[6] = "hello";
printf("%s", c);

还可以这样

1
2
char c[] = "hello";
printf("%s", c);

传递字符数组

由于字符串的最后一个字节有\0,所以我们可以利用最后一个字节为假,作为结束符号。这样,就不用像整型数组一样,传递长度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void print(char c[]) {
int i = 0;
while (c[i])
{
printf("%c", c[i]);
i++;
}
printf("\n");
}
int main() {
char c[20] = "hello";
print(c);
}

输入一行字符

我们可以用gets()来读取一行,它读到\n才开始赋值(并把\n换成\0)。他不像scanf那样,读到空格或\n就赋值

1
2
3
4
5
6
#include<stdio.h>
int main() {
char c[20];
gets(c);
puts(c);
}

字符串操作(str系列)

有const的话,即可以放字符串常量,也可放变量

  • strlen()
    unsigned int strlen(const char *str)

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

  • strcmp()
    int strcmp(const char *str1, const char *str2)

    当str1指向的字符串大于str2指向的字符串时,返回正数。

    当str1指向的字符串等于str2指向的字符串时,返回0。

    当str1指向的字符串小于str2指向的字符串时,返回负数。

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

    此函数原型为 char *strcat(char *dest, const char *src).

    功能为连接两个字符串,把src连接到dest后面;返回dest地址

指针

指针就是地址变量,用于存放地址。

使用场景:传递和偏移

  • 指针变量也是有类型的。(e.g.int*,float *…)

    只能把同类型的地址存放在同类型的指针变量中。

指针的声明

int * p表明

  1. p是指针
  2. *p的类型是int
  3. *是告诉编译器p是一个指针

声明多个指针

int *p1, *p2, *p3

可以看到指针是用于存放地址的,其空间大小,就表示地址总线的条数(1条=1b),32位就是4字节。

1
2
3
4
5
6
7
int* p;
char* c;
printf("%d %d\n",sizeof(p), sizeof(c));
printf("%d %d\n", sizeof(*p), sizeof(*c));
4 4
4 1

注意:指针的声明和使用是两个东西

指针的偏移

对指针变量进行加减运算。如果+1,并不是+1B(按字节编号),而是加了指针类型所占据的字节。

e.g.

1
2
3
int a[5];
int *p=a;
p+1//这里+1就是+4

指针的++和–

任何时候都先把 后++ 去掉,等整个语句运行完后。再按照优先级对相应的变量执行 后++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
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);
return 0;
}
//p先指向a[0], 再取值赋值给j, 最后指向p++,让p指向a[1]
//a[0]=2, j=2,*p=7

指针的注意事项

*p:这个东西好像是一个指针变量的值,但这样理解并不深入。

下面的例子,我们应该把*p==a[0],改变*p就是改变a[0]

1
2
3
int a[3] = { 2,7,8 };
int* p;
p = a;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
int a[3] = { 2,7,8 };
int* p;
int j;

p = a;
j = (*p)++;
printf("a[0]=%d, j=%d,*p=%d\n", a[0], j, *p);
return 0;
}

a[0]=3, j=2,*p=3

内存

  • 代码区:存放代码

  • 数据区:全局变量,字符串常量(程序中所有的相同的字符串常量,只存放一个)。这个区只能读,不能写。

    这里”hello”是字符串常量,p指向数据区,所以不能更改。而char c[20] = "hello";是让字符串常量的每一个字符复制到栈区。所以可以,更改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #define _CRT_SECURE_NO_WARNINGS
    #include<stdio.h>
    #include<string.h>
    int main() {
    char* p = "hello";
    char c[20] = "hello";

    //p[0] = 'a'; //不能对常量更改
    c[0] = 'a'; //可以对栈区更改
    printf(c);
    }
  • 栈空间:整型,浮点型,字符型变量,数组变量,函数,都在栈空间中。

    栈空间在编译的时候就确定好。

  • 堆空间:需要申请,也需要释放

动态申请堆

malloc()

单位:字节B

返回值:无类型指针。所以,要用强制类型转换,转换成指定类型的指针。

calloc()

申请空间,并对空间赋值为0

free()

释放空间必须与原来申请的指针一致,不能偏移。

1
2
3
free(p);
p=NULL;//free完之后,把p置为NULL,否则,就是野指针(指针没有一个确定的地址)

e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main() {
int i;//申请字节数
scanf("%d", &i);
char* p;
p = (char*)malloc(i);//指定指针类型为char*
strcpy(p, "malloc success");
puts(p);
free(p);
p = NULL;
return 0;
}

函数

函数调用的本质

CPU通过PC得到main函数(代码区)的地址,main函数创建栈帧用于存放局部变量。main可能调用其他函数fun,PC就会指向fun的地址,fun函数也会申请栈帧。当fun函数结束,这个栈帧就会释放。(所以,栈区在逻辑上就数据结构上的栈,后进先出),PC就会返回主调函数。

  • 函数传递的参数是值传递,就是复制一个值给函数。

  • 函数在栈中,其栈空间会随函数的调用而开辟,到结束而释放。

  • 注意:自己申请的堆空间不会随函数的结束而释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* my_stack() {
char c[20]= "I am a stack";
puts(c);
return c;
}

int main() {
char* p;
p = my_stack();
puts(p);
}
I am a stack
7Zy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

char* my_heap() {
char* c = (char*)malloc(20);
strcpy(c, "I am a heap");
puts(c);
return c;
}

int main() {
char* p;
p = my_heap();
puts(p);
free(p);
p = NULL;
}
I am a heap
I am a heap

递归的本质

函数调用自己,pc不断重回函数起始位置,但代价是每调用一次,在栈区就会多申请一个栈帧。如果,这样一直调用就会导致栈溢出,所以我们必须设置递归出口。

the process of repeating a function, each time applying it to the result of the previous stage.

要记住的二要素(分治合)

  1. 大问题——>小问题的递推关系(假设小问题已解决)
  2. 找到最小问题

递归就是不断压栈,得到最小问题。然后不断弹栈,计算出更大的问题。

1
2
3
4
void func(int n){//大问题
//设置最小问题出口
func(n-1);//小问题(假设已经解决)
}

n的阶乘

求n!(大问题),小问题是(n-1)!,递推关系:n!=n*(n-1)!

最小问题n=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstdio>
long long fac(long long n){//大问题
if(1==n){//n=1时就走
return 1;
}
return n*fac(n-1);//小问题(假设已解决)
}

int main(){
long long n;
while (scanf("%lld",&n)!=EOF){
printf("%lld",fac(n));
}
}

结构体

  1. 声明结构体类型

    1
    2
    3
    4
    5
    6
    7
    8
    struct student	//struct是关键字,student是数据类型
    {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    }; //最后一定加分号
  2. 定义变量

    1
    2
    struct student s = { 1,"ferry",'m',20,100.0 };//定义,初始化。struct关键字还要写
    struct student sarr[3];

typedef

由于定义一个结构体变量要写struct关键字,和类型,所以很麻烦,所以我们使用typedef来给结构体一个别名。

这样就可以在定义的时候,不用写关键字struct

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct student {
int num;
char name[20];
char sex;
}stu, *pstu;


int main() {
stu s = { 19,"ferry",'M' };
pstu p = &s;

}

结构体的大小

这里涉及到内存对齐的知识。

对齐的目的是为了访问提高cpu访问内存的效率

结构体指针

1
2
3
4
5
6
struct student s = { 1,"ferry",'m',20,100.0 };//定义,初始化。struct关键字还要写

struct student* p;
p = &s;
printf("%d %s %c %d %f", p->num, p->name, p->sex, p->score);
printf("%d %s %c %d %f", (*p).num, (*p).name, (*p).sex, (*p).score);

->与后++优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct student {
int num;
char name[20];
char sex;
};
int main()
{

struct student sarr[3] = {1001,"lilei",'M',1005,"zhangsan",'M',1007,"lili",'F'};
struct student* p=&sarr; //定义结构体指针
int num;
//->比++优先级大
num = p->num++;//先执行num=p->num, 再按优先级加加 p->num++;
printf("num=%d,p->num=%d\n", num, p->num);//1001 1002

num = p++->num;//先执行num=p->num, 再按优先级加加 p++
printf("num=%d,p->num=%d\n", num, p->num);//1002 1005
}

C++的引用

首先要把文件类型改成.cpp

更改值

引用是为了在我们要在子函数中更改变量的时候,不用写指针的*,传递变量名就可以直接更改主函数中变量的值

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
void modifynum(int& i) {
i = 10;
}

int main() {
int i = 5;
modifynum(i);
printf("%d\n", i);
}
10

更改指针

引用是为了在我们要子函数中更改指针的时候,不用写指针的**,传递指针就可以直接更改主函数中的指针

1
2
3
4
5
6
7
8
9
10
11
void modify_point(int*& p) {
p = (int*)malloc(20);
p[0] = 5;
}

int main() {
int i = 5;
int* p = NULL;
modify_point(p);
printf("%d\n", p[0]);
}

小常识

程序阻塞的情况:

  1. I/O操作。(scanf, gets 检测到缓冲区为空)
  2. 无限循环