注意:本文最初使用jupyter notebook编写,后经程序转换为markdown,所以格式可能有多处错误,懒得修改了。
输出字符功能需要与环境进行交互,很复杂,这不是C语言所包含的功能。 练习的过程中需要用到输出显示结果,所以我们有必要先学习一下输出功能。标准库IO库提<stdio.h>
提供了输出函数:
int printf(char *format,arg1,arg2,...)
第一个参数是输出的格式,以后的参数都是要输出的变量
字符 | 输出形式 |
---|---|
d | 十进制数 |
o | 八进制数 |
x | 十六进制数 |
c | 单个字符 |
s | 字符串 |
f | 浮点数 |
p | 指针(地址) |
#include <stdio.h>
int main(){
int a = 1;
char b = 's';
char c\[10\] = "abcdefg";
float d = 1.2;
printf("%i,%c\\n%s\\n%f",a,b,c,d);
return 0;
}
c语言中只有四种基本的数据类型
符号 | 数据类型 | 16位机器 | 32位机器 | 64位机器 |
---|---|---|---|---|
char | 字符型 | 8位 | 8位 | 8位 |
int | 整型 | 16位 | 32位 | 32位 |
float | 单精度浮点型 | 16位 | 32位 | 32位 |
double | 双精度浮点型 | 64位 | 64位 | 64位 |
另外提供short,long,signed,unsigned等限定符。
理论上int的长度应该是cpu一次所能处理的位数,但实际上现在64位cpu中的整型也是32位,因为32位的int足够用,改的话涉及兼容性问题,况且想存储更大的数可以加long修饰符。
c语言与脚步语言的一大不同是变量和函数都需要先声明后使用。
变量声明是为了给变量分配空间。
函数需要先声明是因为函数通常在多个文件中定义,而这些文件还可以单独进行编译,那么单独编译某一个文件的时候如何找到其他文件中的函数?c语言的做法是把所有函数的声明放到一个后缀位.h的文件中,在所有的.c文件中引入这个头文件,这样就可以单独编译任何一个文件。
当然大型项目有很多个头文件,对于中小型程序只需要一个头文件就够了。
文件包含
//在当前目录查找文件
\# include "文件名"
//根据查找规则查找文件
\# include <文件名>
宏定义
\# define maxline 100
\# define max(A,B) (A>B?A:B)
条件包含
//为了让hdr.h只被包含一次,可以这样写
\# if !define(HDR)
\# define HDR
/\* 头文件内容 */
\# endif
指针是一种保存变量地址的变量。
需要注意的是,指针只能指向某种特定的数据类型,整型指针只能指向整型。
\# include <stdio.h>
int main(){
int a =1,b=2;
int *ip; //指向整型的指针
ip = &a; //现在ip存储着变量a的地址
b = *ip; //*ip可以当a用
*ip = 3; //*ip可以当a用
printf("%x,%d,%d",ip,a,b);
}
c语言的函数传参是将参数复制到函数内部的局部变量里,所以在函数中如何折腾都不会改变函数外变量的值。
但是如果想在函数内部修改函数外部的变量怎么办?可以将指针传进函数,然后函数内部用指针修改。
\# include <stdio.h>
void swap(int *px, int *py){
int temp;
temp = *px;
*px = *py;
*py = temp;
}
int main(){
int a =1,b=2;
swap(&a,&b);
printf("%d,%d",a,b);
}
如果一个函数返回指针可以这样定义函数
void *swap(){
...
}
数组是基础数据结构的集合。4种基础数据结构int,char,float,double都可以组成数组。
定义的时候必须声明大小,以便分配空间。
\# include <stdio.h>
int main(){
int *ip;
int a\[10\];
char b\[11\];
float c\[12\];
double d\[13\];
a\[2\] = 123;
b\[10\] = 'c';
c\[6\] = 1.732;
d\[7\] = 1.414;
ip = &a\[2\];
printf("%d",*ip);
}
当我们声明一个数组时,例如int a[4];
,系统做了什么?
系统给我们分配了5个int型变量的空间,再产生一个指针变量static int *a
指向指向该数组第一个元素。
所以,数组名就是一个指针,我们可以像使用指针一样使用数组名。
//取数组第一个元素的地址
pa = &a\[0\];
pa = a;
//取第i个元素的地址
pa = &a\[i\];
pa = a+i;
//取第一个值
b = a\[0\]
b = *a
//数组下标取值 等价于 指针取值
b = a\[i\]
b = *(a+i)
唯一与指针不同的地方是,数组名是一个静态的指针,它不能改变
//指针可以自增指向下一个位置
pa++
//数组不可以自增
a++ //❌
既然数组名是一个指针,那么函数在传参过程中,传递的就是指针。
如果一个函数需要接收一个数组,需要用指针来接收。
int strlen(char *s)
{
...
}
也可以使用char s[]
,与char *s
等价,但后者更直观
单引号代表一个字符,需要用char
来存储。
双引号代表一个字符串,需要用char[]
来存储。字符串的末尾会多存储一个二进制为0的字符\0
,用来标识字符串结束。
#include <stdio.h>
#include <string.h>
int main(){
char str1\[5\] = "12345";
char str2\[6\] = "12345";
printf("strlen(str1)=%d\\n", strlen(str1));
printf("strlen(str2)=%d\\n", strlen(str2));
}
str1没给'\0'留位置,编译器不会报错,但是strlen()会一直找下去,直到遇到'\0'停止,所以这里的strlen(str1)的值是错的。str2多分配了一个字节给'\0',所以strlen(str2)的值是对的。
so,我们为字符串分配空间时可以多分配一个字节,这样有有助于判断字符串的结束。
每个人几乎是刚接触到c语言就用到了字符串常量
\# include <stdio.h>
int main(){
printf("hello word!\\n");
//相当于
char *p = "hello word!\\n";
printf(p);
}
字符串常量是不可以修改的,如果想要修改可以定义成字符数组。
\# include <stdio.h>
int main(){
char message\[\] = "hello word!\\n";
message\[0\] = 'o';
message\[1\] = 'o';
message\[2\] = 'o';
message\[3\] = 'o';
printf("%s",message);
}
c语言支持将指针存储在一个数组中,构成指针数组,它的定义如下:
\# include <stdio.h>
int main(){
char *name\[\] = {
"January","Feb","Mar"
};
printf("%s,%s,%s",name\[0\],name\[1\],name\[2\]);
}
数组名是指针,那么指针数组名当然是指针的指针
char **p;
p = name;
\# include <stdio.h>
int main(){
int arr\[5\]\[5\] = {
{0,0,1,0,0},
{0,0,1,0,0},
{0,0,0,0,1},
{0,1,1,0,0},
{0,1,0,1,0}
};
}
多维数组与指针数组在使用的语法上很类似,但是原理完全不同:
num\[2\]\[3\] //取指针数组中秩为2的指针,再取该指针所指向的数组中秩为3的值
arr\[2\]\[3\] //取多维数组行秩为2,列秩为3的值
指针数组在内存中是一个数组,存了很多指针。通过指针找下一维度的数组。
多维数组是在内存中存了m*n
个整数。它在内存中的存储形式和一维数组一样,只不过可以自动计算偏移量而已。
多维数组名与一维数组名一样,都是指向首元素的指针,我们也可以手动计算偏移位置取值:
int *p;
p = (int *)arr;
printf("%d",*(p+j*3+2)); //取第4行秩为2的元素
当然你也可以这样定义指针,它可以自动计算偏移:
int (*p)\[5\];
p = arr;
printf("%d",p\[3\]\[2\]); //取第4行秩为2的元素
就像数组名是指向数组的指针一样,函数名是指向函数的指针。
唯一不同的是,通过函数指针调用函数可以不加*
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
printf("%p\\n",max);
printf("%d\\n", (*max)(1,6));
printf("%d\\n", max(1,6));
}
通过函数指针可以实现回掉函数的功能
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
void printmax(int x,int y,int max (int x, int y))
{
int a = x + y;
int b = x + 10;
printf("%d\\n", max(a,b));
}
int main()
{
printmax(1,6,max);
printmax(10,6,max);
}
结构体可以理解为脚本语言中类的简化版。
类需要先定义,然后再使用,结构体同样需要先定义后使用。通过结构体.成员
来访问成员。
\# include <stdio.h>
int main(){
//定义结构体
struct point{
int x;
int y;
};
//生成结构体变量
struct point a = {200,200},b = {200,200},c = {200,200};
//定义并生成
struct {
int x;
int y;
} d,e,f;
printf("%d,%d",c.x,a.y);
}
我们知道数组名其实是一个指针,那么结构体名是指针吗?不是的。
结构体相当于一个普通的变量,函数传参会复制整个结构体,但是通常结构体很大,所以常常传递指针给函数。
r->x
等效于(*r).x
\# include <stdio.h>
struct rect{
int x;
int y;
};
int canonrect(struct rect *r)
{
//使用指针和. 访问成员
printf("%d",(*r).x);
//与上述等效
printf("%d",r->x);
}
int main(){
struct rect a = {200,200};
canonrect(&a);
}
联合体是一种非常特殊的结构体。其定义和取值的方法都与结构体相同。他们的不同点是:结构体会为每一个成员变量分配内存空间。联合体只按照最大的成员变量分配一块内存空间,所有的成员变量公用这一块内存空间。很明显,联合体不能保存所有的成员变量,每一时刻只能保存其中一个成员变量。
我们可以将联合体视作一个变量,这个变量可以合法保存多种数据结构的类型。
\# include <stdio.h>
union data{
int a;
char b;
float c;
};
int main(){
union data u;
u.a = 1;
printf("%d, %c, %f\\n", u.a, u.b, u.c);
u.b = 'a';
printf("%d, %c, %f\\n", u.a, u.b, u.c);
u.c = 1.732;
printf("%d, %c, %f\\n", u.a, u.b, u.c);
}
标准库<stdlib.h>中提供了很多实用函数:
#include <stdlib.h>
//产生随机数
int rand(void)
//分配nobj个大小为size的空间
void *calloc(size_t nobj, size_t size)
//分配1个大小为size的空间
void *malloc(size_t size)
//改变已分配内存的大小
void *realloc(void *p, size_t size)
//释放内存
void free(void *p)
//使程序非正常终止
void abort(void)
//使程序正常终止
void exit(int status)
有的时候我们需要在计算机中表示无穷大和无穷小,这时可以用计算机能表示的最大值和最小值代替。
#include <stdio.h>
#include <limits.h>
int main(){
printf("%d\\n",INT_MAX);
printf("%d\\n",INT_MIN);
}