C语言基础

C语言基础

前言

和所有编程语言一样,要认识一个语言的结构得从”hello,world”开始,我们使用C语言来输出”hello,world”吧。代码如下:

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello,world\n");
return 0;
}

通过上面的代码我们初识C语言了,下面是分析。

**分析:**第一行是调用头文件,#include是C语言调用标准头文件的标准方法。stdio.h是标准库,调用了这个库我们才可以使用printf()函数

第二行是主函数,在C语言中可以有好多个函数,当时只能有一个main函数,main函数是程序的入口。main前面的是返回值类型,在好多大学中,教C语言的老师喜欢把前面的类型省略,这个叫缺省(xing),但在C语言标准里,前面的int是不可以去掉的。

第三行是函数内执行的语句,这个是一个输出函数printf(),它的作用是将括号里的数据输出到显示屏中,给用户查看。(具体看第一章)

第四行是返回值。(具体内容在函数那一章再细讲)

**细节:**C语言对语法要求不是那么的高,不像python一样需要严格遵循缩进,C语言需要在函数内执行的语句只需要在 {} 中即可,一行语句执行完加 ; 即可。

要执行C语言的程序必须得进行编译,编译完之后才能运行,简单解释一下为什么要这么做:

计算机它能认识0和1这种低级语言,而我们写代码是用C语言这种高级语言来写的,但计算机不认识,必须需要一个编译器来将高级语言转换为低级语言,这样计算机就可以执行。

C语言编译出来的后缀为: .obj

可运行的程序的后缀为: .exe

这只是一个笔记,笔记主要是将重要的内容记下来,所以好多不重要的内容或者是一看就会的内容我就省略了

一、输出函数

前言

在C语言中是没有自带的输出函数的,而是必须通过调用标准库文件才能使用输出语句,先对输入输出函数进行了解,会使你对C语言有更好的兴趣。

printf();

  • 头文件:stdio.h
  • printf函数的功能:

    格式化输出函数,用于向标准输出设备按规定格式输出信息。

  • printf函数的调用格式:

    printf("格式控制字符串",输出项清单);

    格式控制字符串:用于指定输出格式;由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串。

    输出项清单:

    • 输出项可以是常量、变量或表达式
    • 要求格式字符串和各输出项在数量和类型上应该一一对应

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>

    int main()
    {
    int m=123,n=12345;
    printf("m=%5d,n= %3d\n",m,n);
    return 0;
    }
    1
    cin >> 变量;

对于整形数而言,当格式控制字符规定的长度比数据本身长度还大时,则左侧补空格;若个数控制字符规定的长度比数据本身长度小时,则数据按照其本身长度进行输出。

  • 格式化字符串

    表示方法 说明
    %d 以十进制形式带符号整数(正数不输出符号)
    %o 以八进制形式输出无符号整数(默认不输出前缀0,%#o输出前缀0)
    %x 以十六进制形式输出无符号整数(默认不输出前缀0x,%#x输出前缀0x)
    %u 以十进制形式输出无符号整数
    %f 以小数形式输出单、双精度实数,默认保留6位小数
    双精度型可用%lf或%le
    %e 以指数形式输出单、双精度实数。格式是m.ddddddexxx,默认小数精度为6,指数精度为3,不足补零(明白即可)
    %g 以%f或%e中较短的输出单、双精度实数
    %c 输出单个字符
    %s 输出字符串,直到遇到\0,若字符串长度超过指定的精度则自动突破,不截断
    %p 输出变量的内存地址

putchar();

1.用法

主要是用于输出字符的函数

2.格式

1
2
3
4
5
6
#include <stdio.h>
int main(){
char x = 'A';//一定是字符型变量
putchar("%c", x);//格式化字符串一定要%c
return 0;
}

二、输入函数

scanf();

1.调用格式

1
2
3
4
5
6
#include <stdio.h>//调用头文件
int main(){
int x;
scanf("%d", &x);
return 0;
}

必须要加取值符 &

2.分析

将用户输入的值赋值给取值符后的变量

getchar();

1.用法

输入字符

2.格式

1
2
3
4
5
6
#include <stdio.h>
int main(){
char x;
getchar("%c", &x);
return 0;
}

三、变量

在C语言中有局部变量和全局变量

1.变量的类型

(1)整型变量
类型 储存大小 说明
int 2-4个字节 整型
short 2字节 短整型
long 8字节 长整型
unsigned short 2字节 无符号短整型
unsigned int 2-4字节 无符号整型
unsigned long 8字节 无符号长整型
(2)浮点型
类型 储存大小 说明
float 4字节 单精度浮点数
double 8字节 双精度浮点数
long double 16字节 长双
(3)字符型
类型 储存大小 说明
char 1字节 字符型
unsigned char 1字节 无符号字符型

说明:字符型也可以存放数字类型

2.变量的定义

1
2
类型名 变量名 = 赋的值;
类型名 变量名;

两个都可以定义

1
2
3
1)变量名不能是保留字
2)变量名不能由数字开头
3)变量名只能由下划线,数字,单词组成

注:每次定义后储存的变量一定是同种类型,负责会报错,C语言并没有字符串这个类型,如果要定义字符串需要定义字符型数组

3.变量的转换

例子1:

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
char num1 = 2;
char num2 = 3;
int num3 = num1 + num2;
return 0;
}

这个样子是可以转的,因为 char 类型是1个字节,而 int 为4个字节是可以装的

例子2:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
float f;
int i=5,a;

a=6;
f=a/i;

printf("%f",f);
return 0;
}

如果存储计算后的结果为整型,则会抛弃小数点后的数。

如果为浮点型,则会输出小数点后的数

4.变量的强制转换

有些时候无法通过赋值来进行类型转换

所以这个时候就需要使用强制转换

(1)语法

1
(数据类型) 变量;

5.全局变量和局部变量

(1)全局变量

1
2
3
4
5
6
#include <stdio>
int x = 5;//全局变量
int main(){
printf("%d", x);
return 0;
}

全局变量是定义在函数体外的,在函数体中没办法修改全局变量

(2)局部变量

1
2
3
4
5
6
#include <stdio>
int main(){
int x = 5;//局部变量
printf("%d", x);
return 0;
}

四、常量

1.定义

(1)define定义

1
#define 变量名 值

(2)const关键字

1
const 数据类型 变量名 = 值;

重点在存储类中

五、存储类

1.auto

默认类,就是用完就释放

可写可不写

2.register

感觉没什么用

官方解释说是可以提高运算速度,但现在的CPU运算速度都很快,所以没什么用

3.static

auto 是反过来的,使用完它并不会释放

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void count(){
static int x = 0;
x++;
printf("%d\n", x);
}
int main(){
count();
count();
count();
count();
return 0;
}

4.const

const 是将变量中的值固定,使得变量无法被修改

1
const int a = 20;

但是在vs编辑器中是可以通过指针来修改被const修改的值的

如下

运行结果

运行结果

在dev-c++中就无法使用这个办法进行修改

六、C语言编程结构

在所有编程语言中有着三种结构:顺序结构,选择结构,循环结构,循序结构

1.顺序结构

顺序结构是编程语言中最基础的一种结构,计算机不像人的思维,它得到代码后它没有办法自己行进跳转(有选择和分支除外),它只能一条一条的执行,一条一条执行的情况就是顺序结构,如下代码

它的执行情况如下

1 -> 2 -> 3 -> 4 -> 5 -> 6这样执行的。

这种执行是非常简单也很基础,但没有办法做一些逻辑很强的程序,所以我们引入了选择结构。

2.选择结构

在介绍选择结构前先介绍逻辑判断语句。

2.1 逻辑判断

在C语言中0表示假,1表示真

关系符 a1 a2 结果(a1) (a2)
|| 逻辑或 or 1 1 1
|| 1 0 1
|| 0 0 0
&& 逻辑与 and 1 1 1
&& 1 0 0
&& 0 0 0

然后还有一个逻辑是 ! 逻辑非

a1 a1!
1 0
0 1

补充:在c语言中还有一个对于二进制的逻辑表达式

| :二进制或

& :二进制与

^ :二进制异或

~:二进制非

2.2 逻辑判断

两个数之间判断大小,如果成立则值等于1,如果不成立则等于0。

但在C语言中,只能两个数之间相比,不能多个

如下:

1
a > b;

是可以的,但下面的是绝对不可以

1
a > b > c;
2.3.1 三元运算符
1
int max = (a>b)?a:b;

如果a>b成立,则结果为a,否则返回b

2.3 判断语句

2.3.1 if语句
1
2
3
4
5
6
if(判断条件){
//成立后执行花括号里的语句
执行1;
执行2;
.....
}
2.3.2 else语句

else语句是可以没有判断条件的,但必须有执行语句

2.3.3 else if

用法:

1
2
3
4
5
6
7
8
9
10
11
12
if(判断条件){
//成立后执行花括号里的语句
执行1;
执行2;
.....
}
else if (判断条件){
//成立后执行花括号里的语句
执行1;
执行2;
.....
}
2.3.4 注意

在if语句里可以有也可以没有else、else if语句,但必须要有if语句

2.3.5 switch

switch是单个判断的语句

语法如下:

1
2
3
4
5
6
switch(表达式){
case 常量表达式1:语句1;
case 常量表达式2:语句2;
case 常量表达式3:语句3;
default : 语句n+1;
}

但这个语句执行会一直

例如

然后输出的结果如下

它第一个条件判断成功了,但它却继续执行下面的输出代码。

所以我们会在后面添加一个 break 使它判断成功后执行玩就结束。

如下:

2.3.6 switch判断范围问题

在C语言中,一些时候可以用if-else来判断范围问题,有些时候也可以使用switch来判断范围问题。

使用switch的思路是将个个范围的数化为一个特定的数,然后使用switch语句进行判断。

例如:我们需要计算每个销售数段获得的提成的题目

我们使用if-else语句的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int a;
printf("请输入你总共销售额(单位:万):");
scanf("%d", &a);
if (100 < a && a <=120)
printf("提成:%.2f万", a*0.02);
else if (120 < a && a <= 140)
printf("提成:%.2f万", a*0.05);
else if (140 < a && a <= 190)
printf("提成:%.2f万", a*0.08);
else if (a > 190)
printf("提成:%.2f万", a*0.1);
else
printf("没有提成");
return 0;
}

if-else的写法是非常的简单,但是,如果使用switch来改写这个if-else就有点困难,所以我们要有将范围变成具体的数的思路,然后通过数来进行判断。

switch改写的第一种方法:

我们知道,在C语言中进行的是逻辑判断,通过逻辑判断的出来的只能是或则是,所以我们可以在外部对输入的数进行逻辑判断,然后再乘以具体的1,2,3,因为如果成立则为1,不成立则为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(){
int a, max;
scanf("%d", &a);
max = ((a > 100)&&(a<=120)) + 2*((a>120)&&(a<=140)) + 3*((a>140)&&(a<=190)) + 4*(a>190);
switch(max){
case 1:
printf("提成:%.2f万", a*0.02);
break;
case 2:
printf("提成:%.2f万", a*0.05);
break;
case 3:
printf("提成:%.2f万", a*0.08);
break;
case 4:
printf("提成:%.2f万", a*0.1);
break;
default:
printf("没有提成");
}
return 0;
}

2.4 循环结构

循环结构可以让你完成一件重复的事情而不写多条代码

例如,我们要输出100次“hello,world”,完成这件事需要写100次printf函数和100次“hello,world”,这样子是很麻烦的,但如果有循环结构,那我们就可以很轻松的完成这个程序了。

2.4.1 for循环

for循环是编程语言中最简单的循环了,它的定义如下:

1
2
3
4
5
6
for(初始化值; 循环条件; 值的操作){
循环体1;
循环体2;
循环体3;
.......
}

加入for循环先初始化值,然后判断循环条件,满足条件后运行循环体中的值,运行完之后执行值的操作。重复运行直到不满足循环条件为止。

注意:学过Java的可能会这样写:

1
fot(int i = 0; ;)

虽然C99可以这样写,但是在C语言中并不能这样写

这个标准只能在C++文件中这么使用

2.4.2 while循环

while循环相对于for循环要简单一点,因为只有一个判断条件。

1
2
3
while (判断条件){
循环体;
}

加入循环然后判断是否满足判断条件,如果满足则加入循环体内。

2.4.3 do while循环

do while循环增加了一个do语句

1
2
do{执行语句}
while(判断条件);

先执行do语句内,然后判断是否满足条件,如果满足则继续执行do中的语句。

如果不满足则跳出。

相比于while循环,do while是可以执行一次的。

2.4.4 嵌套循环

简单来说就是循环里套循环,这里借用菜鸟教程的流程图

2.4.5 break和continue
break

break是直接跳出当前循环体

continue

continue是跳过一次

七、数组

1.一维数组的创建和初始化

1.1 数组的创建

1
2
3
4
5
6
//动态创建
类型 变量名[数组的长度(必须是常量)];
//静态创建
类型 变量名[] = {1,2,3};
//不完全初始化
类型 变量名[10] = {1,2,3};

不完全初始化,剩下的元素默认为0

1.2 数组的索引取值

下标从0开始

例子1:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <string.h>
int main(){
char x[] = "hello";
for (int i = 0; i < strlen(x); i++){
printf("%c\n", x[i]);
}
}

例子2:

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
char str[] = {'H', 'e', 'l', 'l', 'o'};
for(int i = 0; i < (sizeof(str)/sizeof(str[0])); i++){
printf("%c\n", str[i]);
}
}

2.二维数组的创建和初始化

2.1数组的创建

1
2
int x[][数组的长度(必须是常量)] = {1,2,3,4,5,6,7,8,9};
int x[数组的长度(必须是常量)][数组的长度(必须是常量)];

二维数组必须初始化行,可以省略列

2.2 二维数组的使用

一样使用索引值

2.3 数组的地址

数组的地址一般都是第一个元素的地址值,之后的地址就是首地址加字节数

注意:有两种情况不是数组的首地址

1.sizeof(数组名) - 数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节

2.&数组名,数组名代表的是整个数组。&数组名,取出的是整个数组的地址

3.数组传入函数中

我们在使用数组的时候,可能会把数组放入函数中进行数据清洗,所以数组如何传入函数就需要说明一下。

3.1 将整个函数复制到函数中使用

第一种传参的方式是将数组中的所有元素拷贝一份到函数中使用

写法

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

这个方法是将数组的地址放入函数中定义的数组中,然后使用里面的元素

3.2 将数组的地址传入函数中使用

这种方式是将数组的地址传入函数中提供,这种方式需要的内存小,推荐使用

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

这种方法相对于只是把数组的地址传入到函数中使用,没有额外创建新的空间。

3.3 数组指针来使用

这个需要等到C语言进阶中才会学到

八、函数

在C语言中,有一个或多个函数,我们最熟悉的main()就是一个函数。

函数的作用是将需要反复使用的代码包含在一起,使我们使用更方便。

1.函数的定义

1
2
3
4
return_type def_name (value1, value2,...){
body of the function;
return value;
}

return_type:返回值的类型

def_name:函数名

value1:形参,如果有多个用英文逗号分开

body of the function:函数体,函数中执行的内容

value:返回值

1.1 返回指针类型的函数

1
2
3
4
return_type* def_name(value1, value2,...){
body of the function;
return value1;
}

value1:为返回的地址

2.函数的使用

2.1 有返回值的函数的使用

1
type variable = def_name(user_value1,user_value2,...);

type:数据类型

variable:变量

user_value1:传入的参数,如果有多个用英文逗号隔开

2.2 无返回值的函数的使用

1
def_name(user_value1,user_value2,...);

variable:变量

user_value1:传入的参数,如果有多个用英文逗号隔开

3.函数的形参和实参

函数的形参可以传入变量和指针,而两种方式需要的内存是不一样的

3.1 函数的形参传入的是变量

这个很容易理解,就是在调用的时候传入的是变量的形式,如下代码:

1
2
3
4
5
6
7
8
void test(int a, int b){
;
}
int main(){
int a = 5, b = 10;
test(a, b);
return 0;
}

这个代码是把a和b的变量传入到函数test中,然后在test中将传入进来的a,b拷贝下来,这样的话需要更多的空间给a,b,非常的消耗空间。

3.2 函数的形参传入的是指针

这个是给函数传入指针变量,如下代码:

1
2
3
4
5
6
7
8
void test(int* a, int* b){
;
}
int main(){
int a = 5, b = 10;
test(&a, &b);
return 0;
}

这个是直接把地址传入到函数里,在函数里只使用传入进去的地址,这样对内存的使用比较小,推荐使用。

4.函数的返回值

我们在使用函数的时候,有些时候是需要返回一些特定的值的,这个时候我们就需要让函数有返回值。

1
return 返回的值;

返回的值一定要和对应的类型相同,这样是规范的写法,如果不同,只要储存的大小合适则还是能运行的。

返回的值也可以是指针类型的

九、指针

1.什么是指针

指针是编程语言中的一个对象,是将内存的地址赋值给指针变量。

它的值直接指向存在电脑存储器中另一个储存单元

1
2
3
4
指针其实是地址
地址就是变量
指针就是变量
存放地址的变量
  • 指针是用于存放地址的,地址是唯一表示一块地址空间的
  • 指针的大小在32位平台是4个字节,在64位平台上是8个字节

指针类型决定了指针进行解引用操作的时候,能够访问空间的大小

int *p 能够访问4个字节

char *p 能够访问1个字节

double *p 能够访问8个字节

指针类型决定了:指针走一步走多远(指针的步长)

int *p; --> 4

char *p; --> 1

double *p; --> 8

总结:指针的类型决定了指针向前或向后走一步有多大(距离)

2.指针的使用

(1)指针的定义

1
2
3
4
5
(指针类型) *(指针名);
//变量方式
(指针类型) *(指针名) = &变量名;
//数组方式
(指针类型) *(指针名) = 数组名;

(2)指针的赋值

1
指针名 = &变量名;

(3)指针类型

1
2
3
4
int    *ip;    /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
1
2
3
4
int* 访问4个字节
float* 访问4个字节
char* 访问1个字节
double* 访问8个字节

(4)如何使用指针

3.野指针

概念:野指针就是指向的位置不可知的

(1)导致野指针的原因

① 未初始化指针

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
int a;
int *p;
printf("%d", *p);
return 0;
}

②指针越界访问

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int arr[10] = 0;
int *p = arr;
for(int i = 0; i <= 11; i++){
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p + i) = 1;
}
return 0;
}

③指针指向的空间释放

(2)如何避免野指针

①指针初始化

②小心指针越界

③指针指向空间释放即用null占位

④指针使用之前检查有效性

1
2
3
4
5
6
7
8
9
10
11
12
int main(){
// int a = 10;
// int *p = &a;//初始化
// int *pa = NULL;//NULL - 用来初始化指针的,给指针赋值
int a = 10;
int *pa = &a;
*pa = 20;
pa = NULL;
if (pa != NULL){
*pa = 20;
}
}

4.指针运算

  • 指针+-整数
  • 指针-指针
  • 指针的关系运算

指针+-整数

1
2
3
4
5
6
7
8
9
10
int main(){
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
int *p = &arr[9];
for(int i = 0; i < 5; i++){
printf("%d\n", *p);
p--;
}
return 0;
}

指针-指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int my_strlen(char *str){
char *start = str;
char *end = str;
while (*end != '\0'){
end++;
}
return end - start;
}
int main(){
//strlen - 求字符串长度
char arr[] = "bit";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}

指针-指针可以得到这个数组的长度 - 1

5.指针和数组

1
2
3
4
5
6
7
8
int main(){
int arr[10] = {0};
printf("%p\n", arr);//地址-首元素的地址
printf("%p\n", &arr[0]);
printf("%p\n", &arr);//整个数组的地址
//1.&arr - &数组名不是首元素的地址-数组名表示整个数组 - &数组名 取出的是整个数组的地址
//2.sizeof(arr) - sizeof(数组名) - 数组名表示的整个数组 - sizeof(数组名)计算的是整个数组的大小
}

总结:数组名表示的是数组首元素的地址

注意:数组可以使用[]来修饰,但是指针使用[]来修饰

6.指针的关系运算

(1)指针数组 && 数组指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//指针数组 - 数组 - 存放指针的数组
//数组指针 - 指针
int main(){
int a = 10;
int b =20;
int c = 30;
// int *pa = &a;
// int *pb = &b;
// int *pc = &c;
//整型数组 - 存放整型
//字符数组 - 存放字符
//指针数组 - 存放指针
int *arr[3] = {&a, &b, &c};//指针数组
for(int i = 0; i < 3; i++){
printf("%d\n", *(arr[i]));
}
return 0;
}

7.二级指针

(1)定义

1
2
3
4
5
6
7
int main(){
int a = 10;
int *pa = &a;
int **ppa = &pa;//ppa就是二级指针
//int ***pppa = &ppa;//pppa是三级指针
return 0;
}

(2)使用

1
2
3
4
5
6
7
8
9
10
11
int main(){
int a = 10;
int *pa = &a;
int **ppa = &pa;//ppa就是二级指针
//int ***pppa = &ppa;//pppa是三级指针
printf("%p\n", pa);
printf("%p\n", *ppa);
printf("%d\n", *pa);
printf("%d\n", **ppa);
return 0;
}

(3)总结

1
p = &a;

这句话的意思是将a的地址给p,然后*p得到的是a中的值。

1
ppa = &pa;

ppa存放的是pa指针中的地址,然后**ppa是pa

8.const修饰指针(一级指针)

通常,我们的指针变量是可以随便使用的,但如果我们想让指针变量或者解引用的指针的内容不改变,那我们就需要使用 const 来修饰指针。

8.1 const 在 * 左边

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

const 在* 左边是修饰 *p的,通过这样修饰后,*p就不能再重新赋值了,*p的值是被固定了,但是指针变量p中的地址是可以重新赋值的。

8.2 const 在 * 右边

1
int* const p;

const 在* 右边是修饰 p的,通过这样修饰后,p就不能再重新赋值了,\p的值是被固定了,但是解引用*p中的值是可以重新赋值的。

9.const修饰指针(二级指针)

在二级指针中有三个位置可以加const修饰符

9.1 在**的左边

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

const在**的左边是修饰**p的,但*p和p的值是可以改变的

9.2 在**中间

1
int* const *p;

const在**的中间是修饰*p的,但**p和p的值是可以改变的

9.3 在**的右边

1
int* *const p;

const在**的右边是修饰p的,但**p和*p的值是可以改变的

9.4 通过二级指针修改被const修饰的一级指针

我们回顾上面讲const修饰符中,别const修饰的变量中的值能被一级指针所修改,那如果别const修改的一级指针能否别二级指针修改呢。

我们来试试:

我们有以下的代码:

1
2
3
4
int main(){
int m = 5, n = 6;
int* const p = &m;
}

在这个语句中,const是修饰指针变量p的,所以我们无法对p变量进行修改。(如下图)

但是如果我们通过二级指针来间接修改呢?

如下代码:

1
2
3
4
5
6
7
int main(){
int m = 5, n = 6;
int* const p = &m;
int* *pp = &pm;
*pp = &n;
printf("%d", *p);
}

我们声明了一个二级指针,然后给这个二级指针赋值一级指针的地址,然后我们对二级指针变量重新赋n的地址值,然后输出*p里面的内容。

然后运行的结果:

我们可以看到,我们输出*p的结果已经改变,所以可以通过二级指针来修改被const修饰的一级指针的值。但是,这个方法只能在vs中才能运行成功,在dev-C++中就不能成功。如下图:

可以看到这个直接就报错了,无法通过编译。

十、结构体

在C语言中,结构是另一种用户自定义的可用数据类型,它允许您存储不同的数据类型,结构体是属于自定义数据结构。

1.结构体的创建

1.1 第一种方法

1
2
3
4
5
struct tag{
type name;
type name;
.........
};

tag 是结构体标签

type 数据类型

name 类型名字

1.2 第二种方法

1
2
3
4
5
struct tag{
type name;
type name;
.........
}tags;

结尾后的tags 是结构的变量,是全局变量,也可以多指很多结构变量。

1.3 全局结构体和局部结构体的声明

全局结构体的声明是声明在函数外,局部结构体是声明在函数内。

2.结构体的使用

2.1 局部结构体的声明 & 初始化

1
struct tag new_name = {value1, value2, {value3, value4}, .....};  

new_name 声明的新名字

value 声明的值,如果有多个值,需要使用{}括起来

value前面要有类型

2.1.1 指针方法

指针声明方法必须先对结构体进行声明,然后才能用指针声明

例一:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
struct Books{
int a;
int arr1[5];
char arr2[5];
};
struct Books book = {1, {4, 3}, {'g', 'a'}};
struct Books* pb = &book;
return 0;
}

struct Books book = {1, {4, 3}, {‘g’, ‘a’}};

struct Books* pb = &book;

这个就是结构体的声明指针方法

2.2 全局结构体全局的使用 & 初始化

和局部结构体的声明和初始化一样

3.结构体的输出

3.1 使用结构体变量的输出

1
结构体声明的变量 . 结构体中的值

使用如下

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
struct Books{
int a;
int arr[5];
}
int main(){
struct Books book;
book.a = 4;
printf("%d", book.a);
return 0;
}

3.2 使用指针变量的输出

3.2.1 第一种方法
1
指针变量 -> 结构体中的值

使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Books{
int a;
int arr1[10];
char b;
};
int main(){
struct Books b;
struct Books* pb = &b;
pb -> a = 1;
printf("%d", pb -> a);
return 0;
}
3.2.2第二种方法
1
(*指针变量) . 结构体中的值

使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Books{
int a;
int arr[2];
char b;
}
int main(){
struct Books b;
struct Books* pb = &b;
(*pb).a = 1;
printf("%d", (*pb).a);
return 0;
}

4.结构体做函数的参数 & 结构体指针做函数的参数

4.1结构体变量做参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Books{
int a;
int arr[5];
char name[5];
};
void Print(struct Books b){
printf("%d,%c", b.a, b.name[0]);
}
int main(){
struct Books book = {5, {4,2,3}, {'s','a','v'}};
Print(book);
}

4.2结构体指针做参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Books{
int a;
int arr[5];
char name[5];
};
void Print(struct Books* b){
printf("%d,%c", b->a, b->name[0]);
}
int main(){
struct Books book = {5, {4,2,3}, {'s','a','v'}};
Print(&book);
}

4.3 两种定义在内存中的模式

4.1的方式是在函数中将变量又重新拷贝一份,占用更多的空间,而且用完就会被释放,不能修改结构体内的值;而4.2的方法是将结构体的地址放入函数里,函数操作地址,内存的占用非常的少,能直接修改函数体内的值。

十一、debug和release方法

1.debug

debug方法是提供给程序员调试的方法。调试时会生成很多配置文件。不会做任何优化

2.release

release是提供用户使用的

十二、文件操作

在C语言中可以很简单的打开文件进行一些操作,这里主要是在Windows环境下使用标准输入输出库函数的对文件操作。

1.文件打开

对文件操作的基础是将文件打开,如果不打开文件就没办法对文件中的内容进行操作。

文件打开的方法:

1
FILE* fopen(const char * __restrict__ _Filename,const char * __restrict__ _Mode)

第一个参数是你需要打开文件的路径,这里的路径可以是绝对路径或者是相对路径。

第二个参数是打开文件的方式,有下面几种方式

模式 说明
r 只读方式
w 写入文件,如果文件不存在会创建出一个文件,如果文件中有内容会删除里面的内容
a 追加模式,如果文件不存在会创建出一个文件,如果里面有数据就往后写入
r+ 允许读写文件
w+ 允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件
a+ 允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

例如我打开一个文件:

1
2
FILE* fd = NULL; // 创建一个FILE类型的指针
fd = fopen("E:\\a.txt", "a");

上面的代码就是打开一个文件,以追加的方式进行打开的。

2.关闭文件

当我们打开文件后进行操作后是需要关闭,不关闭文件会导致文件一直处于一种打开状态,所以需要关闭文件,关闭文件的函数如下:

1
int fclose( FILE *fp );

参数是刚才打开的文件,如果关闭文件出问题,这个函数会返回EOF,但是基本上关闭都是会成功的。

1
fclose(fd);

3.写入内容

打开文件后可以向文件中写入内容,写入的函数如下:

1
int fputc( int c, FILE *fp );

这个函数是可以向文件中光标位置处写入一个字符,比如说在文件中写入一个a字符,那代码如下:

1
fputc('a', fd);

如果要写入字符串也是可以的,使用下面的函数就可以写入一个字符串:

1
int fputs( const char *s, FILE *fp );

比如说我要写入一个”hello”,那语法如下:

1
fputs("hello", fd);

4.读取内容

使用的函数是:

1
int fgetc( FILE * fp );

这个函数可以读取文件中光标后的一个字符,读取后的内容是以返回值的方式进行输出,但是输出不了中文,它返回的只能是ASCII。

如果想读取中文,需要使用读取字符串的函数,函数如下:

1
char *fgets( char *buf, int n, FILE *fp );

这个函数是可以读取字符串,返回的就是你读取的字符串。

第一个参数是字符串存放的位置,需要定义一个数组进行接收。

第二个参数是读取字符串的个数。

第三个参数是文件指针。

比如说我读取一个文件中的内容:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
FILE* fd = NULL;
char buf[20];
fd = fopen("E:\\a.txt", "r"); //以r的方式打开文件
fgets(buf, 20, fd);
printf("%s\n", buf);
// 关闭文件
fclose(fd);
fd = NULL;
}

5.设置光标位置

上面一直提到光标,在文件中光标的位置是非常重要的,就如同我们向一个txt文件中写入内容是需要设置光标位置的,光标的位置是你输入字符进入的位置。

设置光标位置的函数是:

1
int fseek(FILE * fp, long offset, int whence);

其中最后的一个参数是需要使用宏定义的,宏定义如下:

SEEK_SET:设置光标在开头位置

SEEK_CUR:设置光标在指定位置

SEEK_END:设置光标在最后的位置

最后这个设置光标在指定位置第二个参数才有效。