编程思路:变量>算法>流程图>程序
- 用编程解决实际问题需要考虑的两个问题:
1,解决过程中会得到什么数据,怎么读取(设置变量)
2,有了数据后怎么计算 - 计算机程序描写的是步骤,不是关系(多路if 和 if,else if)
文章目录
1,编译调试错误解决:
- gcc编译错误:
undefined reference to `fmod'
collect2: error: ld returned 1 exit status
链接器ld无法在程序中找到例程fmod。您必须告诉它使用-l标志与数学库libm链接。
gcc -lm ***.c
- 函数或变量使用前没有声明 / 某个地方缺少括号:
error:expected declaration or statement at end of input
2,小知识和技巧:
-
C语言是强类型语言(函数或变量使用前先声明)
-
定义变量的时候,最好初始化
-
设置标志作循环条件/判断(证伪先设为1,证真先设0)
(每轮循环末尾再把标志设置为初值) -
测试程序常使用边界数据,如有效范围两端的数据、特殊的倍数等
-
如果要模拟运行一个很大次数的循环,可以模拟较小的循环次数,然后作出推断。
-
sizeof给出目标所占内容大小,单位是字节:如sizeof(a)/sizeof(a[0])
-
c语言中,堆栈stack从高地址往低地址分配内存
-
数组作为函数参数时,往往必须用另一个参数传入数组大小。
-
C 语言中,数组必须是静态的,数组的大小必须在程序运行前就确定下来。
C 语言并不检验数组边界,数组的两端都有可能越界,从而使其他变量的数据甚至程序代码被破坏。检验数组的边界是程序员的职责。 -
做求和程序时,记录结果的变量应该初始化为0,
而做求积的变量时,记录结果的变量应该初始化为1。 -
计算之前先保存原始的值,后面可能有用。
-
循环要有改变条件的机会
-
多层循环遍历,每一轮都需要把内层循环数置初值
如:
while(a<=9)
{
while(b<=9)
{
while(c<=9)
{
....
c++;
}
b++;
c=0;
}
a++;
b=0;
c=0;
}
- 遍历数组技巧:在数组内放一个非有效值,以遍历到这个值作为终止条件。
例:
char a[] = {0,1,2,3,4,5,6,7,8,9, -1};
char *p = a;
//方法一:
for(p=a; *p != -1; )
{
printf("%d\n", *p++);
}
//方法二:
while(*p != -1)
{
printf("%d\n", *p++);
}
-
sizeof
的返回值是无符号数unsigned
-
遍历时注意循环变量是递增
++
还是递减--
-
递归要有结束递归的基准条件
-
警惕程序语法正确 逻辑错误
-
当一个函数结束后,它的本地变量的地址会重新分配给其他函数的本地变量
-
单一出口原则
-
常量符号化:用符号而不是具体的数字来表示程序中的数字
-
避免一专多用(变量)
-
scanf读入一个单词(到空格、tab或回车为止)
-
scanf是不安全的,因为不知道要读入的内容的长度
最后一个输出末尾不带空格:
1,以数组大小size-1作if、else判断,如果与数/下标相等就不输出空格,如果不等就输出空格(空格后置)
for(i=0; i<keyn; i++)
{
if(i==keyn-1)
{
printf("%d",key[i]);
}
else
{
printf("%d ",key[i]);
}
}
2,设置标志,进入循环,判断标志,如果没改动则先输出不带空格的情况,同时修改标志,如果有改动则输出带空格(空格前置)
int flag = 0;
if(flag == 0){
printf("%d",a[i]);
flag = 1;
}
else{
printf(" %d",a[i]);
}
数组循环存储
数组下标加一,然后对数组大小取余,如(5+1)%6==0
(n+1)%len
3,循环:while(当) 和 for(对于)
-
for( ; 条件 ; ) == while(条件)
-
for括号中任一项都可以省略(至少留一个),3个分号不能少
-
for( 初始动作a; 条件b; 每轮的动作c) {…循环体…}
对于一开始的a,
当条件b成立时,重复做循环体,每一轮循环在做完循环体内语句后,再进行c操作,然后再判断条件b,成立就进行下一轮循环,否则退出循环。 -
如果有固定次数,用for
如果必须执行一次,用do_while
其他情况用while -
一次退出多重循环,可以用多个break接力:(也可以用goto) 最内层循环改变标志,然后break,其他所有外层循环依次判断标志,如果是最内层改变的值,就继续break。
4,指针和数组:
指针最常见的错误:
- 定义了指针变量,还没有指向任何变量, 就开始使用指针
- 指针定义后要么设置为
NULL
,要么指向合法内存,否则可能变野指针。
指针应用场景:
1,函数返回多个值,某些值就只能通过指针返回(传入的参数实际上是需要保存待会的结果的变量)
这时最好的方式是:函数return返回状态(一般与主调函数内if语句匹配),指针传回值。
常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:( -1 或 0 ),但是当任何数值都是有效的可能结果时,就得分开返回了。
2,需要传入较大的数据时,用作参数
3,传入数组后对数组做操作
4,动态申请的内存
- 函数参数表中的数组其实是指针:所以在函数内对数组求sizeof,结果是数组指针的大小(64位架构为64/8=8,32位为32/8=4),而不是数组本身大小。
数组指针可以用数组运算符[]运算。 - 数组的变量是特殊的指针: int a[10]; int *p = a;
数组的单元表达的是变量,需要用符号&取地址 a ==&a[0] - 方括号[]运算符可以对指针做也可以对数组做: p[0]<==>a[0]
- 星号*取值运算符可以对指针做,也可以对数组做: 如 *a = 25
- 数组变量是const指针,不能被赋值:int a[] <==> int *const a;
如 int a[] = {.....}; int b[]; 错误操作:b = a
指针的运算:
1,给指针加、减一个整数(+,+=,-,-=)
2,递增、递减(++/–),(指针加1代表地址加sizeof(类型))
3,两个指针相减(结果是:地址差 / sizeof(类型))
4,指针比较:
<,<=,==,>,>=,==,!=
,比较它们在内存中的地址,数组中的单元地址是线性递增的
5,*p++
:(++
优先级比*
号高)
取出p所指的数据,然后把p移到下一个位置,常用于数组类的连续空间操作,在某些CPU上,可以直接被翻译成一条汇编指令。
6,指针运算与数组对应的关系:*(p+n) <==> a[n]
0
地址:
内存中有0
地址,但是0
地址通常是不能随便碰的地址,所以你的指针不应该具有0
值。
因此可以用0地址来表示特殊的事情:
1,返回的指针是无效的
2,指针没有被真正初始化(先初始化为0)
NULL
是一个预定定义的符号,表示0地址(有的编译器不愿意你用0
来表示0
地址)
指针的类型:
无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。
但是指向不同类型的指针是不能直接互相赋值的,避免用错指针。
指针的类型转换:
void*
表示不知道指向什么类型的指针,计算时与char*
相同(但不相通)
指针也可以转换类型:
int *p = &i;
void *q = (void*)p;
这并没有改变p
所指的变量的类型,而是让人用不同的眼光通过q看它所指的变量。(*p
是将&i
地址中的值按照int型变量进行解释,而*q
则是将&i
地址中的值按照void型变量进行解释)
const定义指针的不同情况:
1,指针不可修改(const
在*
后面):表示一旦得到某个变量的地址,不能再指向其他变量
int * const q = &i; //q是const
*q = 26; // OK!
q++; //ERROR!
2,通过指针不可修改(const
在*
前面):表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const),而指针可以重新指向其他变量
const int *p = &i; <==> int const * p = &i;//等效
*p = 26; //ERROR! (*p是const)
i = 26; //OK!
p = &j; // OK!
野指针的三种情况:
1,指针没有被初始化,那么它将指向的是一片未知空间
指针定义后要么设置为NULL
,要么指向合法内存
char *p = NULL;
char *p1 = (char *)malloc(100);
2,指针被free
或delete
后,没有置为NULL
,让人以为是一个合法指针
3,指针超越了变量的作用域
函数的指针
void f(void)
{
printf("hello\n");
}
void (*pf)(void) = f;
函数的指针数组
void f(int i)
{
printf("in f(), %d\n", i);
}
void g(int i)
{
printf("in g(), %d\n", i);
}
void h(int i)
{
printf("in h(), %d\n", i);
}
void (*fa[])(int) = {f,g,h};
求数组大小:
sizeof(fa)/sizeof(fa[0])
函数做参数传给另一个函数(指针)
#include <stdio.h>
int plus(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
}
void cal(int (*f)(int, int))
{
printf("%d\n", (*f)(2,3));
}
int main(void)
{
cal(plus);
cal(minus);
return 0;
}
可变数组
/* ------- 头文件 array.h --------- */
#ifndef _ARRAY_H_
#define _ARRAY_H_
typedef struct {
int *array;
int size ;
} Array; //定义结构
Array array_create(int init_size); //创建结构变量
void array_free(Array *a); //释放结构变量占用的内存
int array_size(const Array *a); //返回当前数组大小
int* array_at(Array *a, int index); //返回指向索引处值的指针
void array_inflate(Array *a, int more_size); //让数组变大(新建一个更大数组,把原数组内容复制过来)
#endif
/* -------------------------------
主程序
------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include "array.h"
const BLOCK_SIZE = 20; //块block的概念
//typedef struct {
// int *array;
// int size ;
//} Array;
Array array_create(int init_size)
{
Array a;
a.size = init_size;
a.array = (int*)malloc(sizeof(int)*a.size);
return a;
}
void array_free(Array *a)
{
free(a->array);
a->array = NULL;
a->size = 0;
}
//封装
int array_size(const Array *a)
{
return a->size;
}
int* array_at(Array *a, int index)
{
if(index >= a->size)
{
array_inflate(a, (index/BLOCK_SIZE + 1)*BLOCK_SIZE - a->size);
}
return &(a->array[index]);
}
//int array_get(const Array *a, int index)
//{
// return a->array[index];
//}
//
//void array_set(Array *a, int index, int value)
//{
// a->array[index] = value;
//}
void array_inflate(Array *a, int more_size)
{
int *p = (int*)malloc(sizeof(int)*(a->size + more_size));
int i;
for(i=0; i<a->size; i++)
{
p[i] = a->array[i];
}
free(a->array);
a->array = p;
a->size += more_size;
}
int main(int argc, char const *argv[])
{
Array a = array_create(100);
printf("%d\n", array_size(&a));
*array_at(&a,0) = 10;
printf("%d\n", *array_at(&a,0));
int number;
int cnt = 0;
while(number != -1)
{
scanf("%d", &number);
if(number != -1)
{
*array_at(&a, cnt++) = number;
}
//scanf("%d", array_at(&a, cnt++));
}
array_free(&a);
return 0;
}
5,动态内存分配与回收:
(如果malloc
后没有free()
,程序完全结束后会统一释放空间。但对于大型程序来说,最好要合理free(),否则容易出错)
- 输入数据
如果输入数据时,先告诉你个数,然后再输入,要记录每个数据
C99可以用变量做数组定义的大小
C99之前只能malloc
动态申请:
int *a = (int*)malloc(n*sizeof(int));
malloc申请内存空间:(堆区分配,作用范围全局)
头文件#include <stdlib.h>
原型void * malloc(size_t size);
向malloc
申请的空间是以字节Byte
为单位,而不是类型单元为单位(如int、char…)
返回的结果是void*
,需要类型转换为自己需要的类型
(int*)mallloc(n*sizeof(int));
没空间了:
如果申请失败则返回0
,或者叫做NULL
试试你的系统可以申请多少内存?
free()释放内存:
把申请得来的空间还给系统
申请过的空间,最终都应该要还
只能还申请来的空间的首地址
free(NULL);
不会出错
常见问题:
1,申请了没free
——>长时间运行内存逐渐下降
新手:忘了
老手:找不到合适的free
的时间
2,free
过了再free
3,地址变过了, 直接去free
解决方法:
1,每一次malloc
之后,都配对一个free
2,合理设计程序架构,有合适的时机free
3,经验(实践,阅读优秀代码)
6,字符:
- 注意scanf双引号内参数:引号内所有字符都会按顺序读取输入(包括空格)
scanf("%d %c", i, c);
scanf("%d%c", i, c);
- putchar
int putchar(int c);
向标准输出写一个字符
返回写了几个字符,EOF(-1)
表示写失败 - getchar
int getchar(void);
从标准输入读入一个字符
返回类型是int是为了返回EOF(-1)
Windows–>Ctrl+Z
Unix–>Ctrl+D - 字符运算:加减数字, 互减
一个字符加一个数字得到ASCII码表中那个数之后的字符
两个字符相减,得到它们在表中的距离 - 字符大小写转换:
大写转小写a + 'a' - 'A'
小写转大写a + 'A' - 'a' <==> a - ('a' - 'A')
- 逃逸字符:
\b \" \' \\ \t \n \r
回退符\b会用它的后一个字符替换前一个字符
(如果它后面是换行符\n,则不会删除前面的字符)
制表符\t代表行中的固定位置,而不是固定大小字符数量。
7,字符串与字符数组:
- 以0或’\0’结尾的一串字符,但不是’0’
0有可能占4字节(int), '\0’只占一个字节
- 0标志字符串的结束,但它不是字符串的一部分,
计算字符串长度的时候不包含这个0 - 字符串以字符数组的形式存在, 以数组或指针的形式访问
(更多的是以指针的形式) - string.h 里有很多处理字符串的函数
- 不能用运算符对字符串做运算
- 通过数组的方式可以遍历字符串
- 唯一特殊的地方是字符串字面量可以用来初始化字符数组
- 两个相邻字符串会自动拼接: “第一个字符串”“第二个字符串”
- 换行拼接可以用反斜杠\连接:(注意:第二行开头的空格、制表符也会显示打印),如:
"Hello\
world!"
-
字符串有两种表现形式:
1,指针char *s = "Hello World"
; 不能修改字符串内容指针应用:这个字符串不知道在哪里,处理参数(函数),动态分配空间(malloc)
2,数组
char w[] = "Hello World"
;可以修改字符串内容数组应用:这个字符串在这里,作为本地变量空间自动被回收
-
如果要构造一个字符串,用数组。
如果要处理一个字符串,用指针。 -
关于
char*
是不是字符串类型 :
字符串可以表达为char*
的形式,char*
不一定是字符串
本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
只有它所指的字符数组有结尾的0,才能说它所指的是字符串 -
strlen
和sizeof
:
strlen字符串的有效长度(不包括结尾的’\0’)
sizeof求字符串所占内存大小(包括结尾的’\0’) -
合理的字符串初始化结尾要有
'\0'
:
1, 能加'\0'
的情况:
1,char a1[] = {'h','e','l','l','o','\0'};手动加'\0'.
2,char a2[6] = {'h','e','l','l','o'};给数组预留空位
3,char a3[] = {"hello"};或char a4[] = "hello";一般用前者
2, 不会加'\0'
的情况
char a5[] = {'h','e','l','l','0'}
- 字符指针只能采用字符串的方式初始化:结尾都会自动添加’\0’
char *str = "hello";
或者
char *str; str="hello";
- 复制一个字符串:
char *dst = (char*)malloc(strlen(src)+1);
strcpy(dst,src)
- 字符串中找字符串:
char *strstr(const char *s1, const char *s2);
char *strcasestr(const *s1, const char *s2);(忽略大小写)
-
空字符串:
char buffer[100]=""; 这是一个空的字符串,buffer[0]=='\0'
char buffer[] = ""; 这个数组的长度只有1
-
字符串数组:
char **a;
a是一个指针,指向另一个指针,那个指针指向一个字符(串)
1,char a[][10]
;(列数必须给出,否则出错)
2,char *a[]
;(指针数组,每一个数组元素都是指针a[0] -- > char *
)
1和2是不一样的 -
程序参数:
int main(int argc, char const *argv[])
argv[0]
是命令本身:
当使用Unix的符号连接时,反映符号链接的名字
8,枚举enum:
枚举是一种用户定义的数据类型,它用关键字enum
以如下语法来声明:
enum 枚举类型名字{名字0,...,名字n};
枚举类型名字通常并不真的使用,要用的是大括号里的名字,因为它们就是常量符号,它们的类型是int,值则依次从0到n。如
enum colors{red, yellow, green};
//创建了三个常量,red的值是0,yellow是1,green是2
当需要一些可以排列起来的常量值时,定义枚举的意义就是给了这些常量值名字
枚举量可以作为值
枚举类型可以跟上enum
作为类型
但时实际上时以整数来做内部计算和外部输入输出的
如:
enum color {red, yellow, green};
void f(enum color c);
C语言内部把枚举就是int
自动计数的枚举:
最后一个枚举量的值,就是它前面所有枚举量的个数。
这样需要遍历所有的枚举量或者需要建立一个用枚举量做下标的数组的时候就很方便。
enum COLOR {RED, YELLOW, GREEN, NumCOLORS};
char *ColorsNames[NumCOLORS];
for(i=0; i<NumCOLORS;...)
声明枚举量可以指定值:
enum COLOR{RED=1, YELLOW, GREEN = 5};
如果有意义上排比的名字,用枚举比const int方便
枚举比宏(macro)好,因为枚举有int类型
枚举有符号,根据数值分配内存。
9,结构struct:
(复合数据类型)
- 结构是语句,大括号后要加分号
;
。
struct date{
int month;
int day;
int year;
};
- 和本地变量一样,在函数内部声明的结构类型只能在函数内部使用, 所以通常在函数外部声明结构类型,这样就可以被多个函数所使用了。
声明结构的形式:
- 形式一:
struct point {
int x;
int y;
};
struct point p1,p2;
p1和p2都是point,里面有x和y的值
- 形式二:
struct {
int x;
int y;
} p1, p2;
p1和p2都是一种无名结构,里面都有x和y
- 形式三:
struct point {
int x;
int y;
} p1, p2;
p1和p2都是point,里面都有x和y
结构变量
- 结构类型和结构变量是两回事
结构类型是结构变量的规范,没有实体。
结构变量才有实体,可以参与运算、赋值等操作。
声明结构类型后,可以用这种结构类型去定义很多结构变量,每一个结构变量都按照结构声明的样子。
结构的初始化
struct date {
int month;
int day;
int year;
};
- 两种初始化:
struct date today = {07,31,2014};
struct date thismonth = { .month = 7, .year=2014};
结构赋值
struct date {
int month;
int day;
int year;
};
struct date today;
today = (struct date){07,31,2014};
//赋值结构时(非初始化,非赋值结构变量)需要强制转换
struct date day; //结构之间赋值
day = today; //只是复制了值,但存储地址不一样
day.year = 2015
结构成员
1,结构和数组有点像
2,数组用 [] 运算符和下标访问其成员:a[0] = 10;
3,结构用 . 运算符和名字访问其成员:
today.day
student.firstName
pl.x
pl.y
结构体字节对齐规则与内存大小计算
对齐规则
a.第一个成员起始于0偏移处;
b.每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐;
c.结构体总长度必须为所有对齐参数的整数倍;
d.对于数组,可以拆开看做n个数组元素。
内存大小计算
struct A
{
int a;
double b;
char c;
};
struct B
{
double b;
char c;
int a;
};
int类型一般是占用四个字节,char类型一般占用一个字节,double类型一般占用8个字节。
1、结构体A首先给int a 分配四个字节,并且以4个字节对齐;然后给double b分配8个字节,发现以4个字节对齐不行,就以8个字节对齐,前面只有int a ,所以int a将占用8个字节;最后为了对齐,将给char c 也分配8给字节,所以结构体A占用了24个字节。
2、结构体B首先给double 分配8个字节,并且以8给字节对齐;然后给char c分配8给字节;最后给int a分配空间的时候发现,前面空有7个字节空间可以放下int a,int a 就和char c一起占用8个字节,所以结构体B占用了16个字节
结构运算
1,要访问整个结构,直接用结构变量的名字
2,对于整个结构,可以做赋值、取地址,也可以传递给函数参数
p1 = (struct point){5,10}; //相当于p1.x=5,p1.y=10;
p1=p2; //相当于p1.x=p2.x; p1.y=p2.y;
指向结构的指针
- 和数组不同,结构变量的名字并不是结构变量的地址,必须使用
&
运算符 - 用
->
表示指针所指的结构变量中的成员
struct date {
int month;
int day;
int year;
} myday;
struct date *p = &myday;
(*p).month = 12;
p->month = 12;
结构作为函数参数
int numberofDays(struct date d);
- 整个结构可以作为参数的值传入函数
- 此时是在函数内新建一个结构变量,并复制调用者的结构的值
- 可以返回一个结构
- 这与数组完全不同
结构指针作为参数
void main()
{
struct point y = {0, 0};
intputPoint(&y);
output(y);
}
struct point* intputPoint(struct point *p)
{
scanf("%d", &(p->x));
scanf("%d", &(p->y));
return p;
}
输入结构
1,没有直接的方式可以一次scanf一个结构
2,无法通过把结构传入函数来读入结构,因为函数参数只是赋值了该结构的值,而不是地址。 C在函数调用时是传值的
- 解决方案:
1,在输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者。
2,把结构指针作为参数传入函数,再通过指针输入值。
结构数组
struct date dates[100];
struct date dates[] = {
{4,5,2005},{2,4,2005}};
结构中的结构(嵌套)
//struct dateAndTime {
// struct date sdate;
// struct time stime;
//};
struct point {
int x;
int y;
};
struct rectangle {
struct point pt1;
struct point pt2;
};
struct rectangle r;
r.pt1.x, r.pt1.y;
r.pt2.x, r.pt2.y;
struct rectangle r, *rp;
rp = &r;
r.pt1.x <==>rp->pt1.x等价
(r.pt1).x<==>(rp->pt1).x等价
- 结构中的结构的数组:
#include <stdio.h>
struct point{
int x;
int y;
};
struct rectangle {
struct point p1;
struct point p2;
};
void printRect(struct rectangle r)
{
printf("<%d,%d>to<%d,%d>\n", r.p1.x, r.p1.y,r.p2.x,r.p2.y);
}
int main(int argc,char const *argv[])
{
int i;
struct rectangle rects[] = {{{1,2},{3,4}},{{5,6},{7,8}}}; //2 rectangles
for(i=0; i<2; i++)
{
printRect(rects[i]);
}
return 0;
}
11,联合:union
union AnElt {
int i;
char c;
} elt1, elt2;
elt1.i = 4;
elt2.c = 'a';
elt2.i = 0xDEADBEEF;
存储:
- 所有的成员共享一个空间
- 同一时间只有一个成员是有效的
- union的大小是其最大的成员
sizeof(union ...) = sizeof(每个成员)的最大值
初始化
- 对第一个成员做初始化
常用场合
- 作为读写媒介:以
字节
的方式读取整数int
或者浮点数double
#include <stdio.h>
typedef union {
int i;
char ch[sizeof(int)];
} CHI;
int main(int argc, char const *argv[])
{
CHI chi;
int i;
chi.i = 1234;
for(i=0; i<sizeof(int); i++)
{
printf("%02hhX", chi.ch[i]);
}
printf("\n");
return 0;
}
12,自定义数据类型:typedef
C语言提供了typedef
功能来声明一个已有的数据类型的新名字,
改善了程序的可读性,如:
typedef int Length;
typedef long int64_t;
typedef struct ADate {
int month;
int day;
int year;
} Date; --->简化了复杂的名字
typedef struct ADate Date;//等同上面的定义
typedef char* Strings[10];//Strings是10个字符串的数组的类型
使得新名字成为已有类型的别名,然后新名字就可以代替原类型出现在变量定义和参数声明的地方:
Length a,b,len;
Length numbers[10];
int64_t i = 100000000000;
Date d = {9, 1, 2005};//用结构类型的新名字Date定义初始化结构变量d
13,链表
用结构struct实现,两个成员,一个存放值,一个指向下一个节点(结构)
// ------------Node.h--------
#ifndef _NODE_H_
#define _NODE_H_
typedef struct _node {
int value;
struct _node *next;
} Node;
#endif
//-----------Node.c------------
/*
链表的函数,版本1
*/
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
//typedef struct _node {
// int value;
// struct _node *next;
//} Node;
Node* add(Node *head, int number);
int main(int argc, char const *argv[])
{
Node *head = NULL;
int number;
do {
scanf("%d", &number);
if(number != -1)
{
head = add(head, number); //链表添加
}
} while(number != -1);
return 0;
}
Node * add(Node *head, int number) //传入的是指针,函数内对指针的值修改无法影响主调函数内指针的值 ,所以需要返回指针
{
// add to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node *last = head;
if(last)
{
while(last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
head = p;
}
return head;
}
/*
链表的函数,版本2
*/
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
//typedef struct _node {
// int value;
// struct _node *next;
//} Node;
void add(Node *head, int number);
int main(int argc, char const *argv[])
{
Node *head = NULL;
int number;
do {
scanf("%d", &number);
if(number != -1)
{
head = add(&head, number); //----与版本1区别 ---
}
} while(number != -1);
return 0;
}
void add(Node **pHead, int number) //----与版本1区别 ---传入指向指针的指针
{
// add to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node *last = *pHead;
if(last)
{
while(last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
*pHead = p;
}
}
/*
链表的函数,版本3
*/
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
//typedef struct _node { 头文件有定义,所以注释掉
// int value;
// struct _node *next;
//} Node;
typedef struct _list { //自定义数据列表,代表整个链表
Node* head;
} List;
void add(List* plist, int number);
int main(int argc, char const *argv[])
{
List list;
int number;
list.head = NULL;
do {
scanf("%d", &number);
if(number != -1)
{
add(&list, number); //链表添加
}
} while(number != -1);
return 0;
}
void add(List* plist, int number)
{
// add to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node *last = plist->head;
if(last)
{
while(last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
plist->head = p;
}
}
/*
链表的函数,版本4,正确性待验证!!!!!
*/
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
//typedef struct _node { 头文件有定义,所以注释掉
// int value;
// struct _node *next;
//} Node;
typedef struct _list { //自定义数据列表,代表整个链表
Node* head;
Node* tail;
} List;
void add(List* plist, int number);
int main(int argc, char const *argv[])
{
List list;
int number;
list.head = list.tail = NULL;
do {
scanf("%d", &number);
if(number != -1)
{
add(&list, number); //链表添加
}
} while(number != -1);
return 0;
}
void add(List* plist, int number)
{
// add to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
if(plist->tail)
{
//attach
plist->tail->next = p;
plist->tail = p;
}
else
{
plist->head = p;
plist->tail = p;
}
}
14,关于const:
非const的值总是可以转换成const:
void f(const int* x);
int a = 15;
f(&a); //OK
const int b = a;
f(&b); //OK
b = a + 1; //ERROR!
节省空间
- 当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改
保护数组值
- const数组:
const int a[] = {1, 2, 3, 4, 5, 6};
数组变量已经是const
的指针了, 这里的const
表明数组的每个单元都是const int
为了保护数组不被函数破坏, 可以设置参数为const。
int sum(const int a[], int length);
const和宏定义的区别:
1,const进行类型检查,宏不会
2,const编译运行时使用,宏在预处理阶段展开
3,const可以节省空间,避免不必要内存分配,修饰常量时,只在初次赋值给变量时分配一次内存,接下来使用不会再分配;宏定义则是引用多少处,展开多少处。
如:
const int i = 6;
int j = i;此时为i分配内存,以后不在分配
int k = i;没有内存分配
#define MAXN 10
int s = MAXN;编译期间进行宏替换,分配内存
int t = MAXN;再次进行宏替换,又一次分配内存
15,变量
无符号数unsigned
- 无符号关键字unsigned,只适用于
int short long char
四种变量 - 浮点型数据只有有符号类型
unsigned a;
是unsigned int a;
的缺省写法
全局变量
全局变量定义和特性
- 定义在函数外面的变量是全局变量
- 全局变量具有全局的生存期和作用域
它们与任何函数都无关
在任何函数内部都可以使用它们
全局变量初始化
- 没有做初始化的全局变量会得到0值
指针会得到NULL值 - 只能用编译时刻已知的值来初始化全局变量
- 它们的初始化发生在
mail
函数之前
extern引用
- 当引用另一个.c源文件中的全局变量时,就必须用extern
- 同一个源文件中,全局变量声明定义在引用函数之前,不需要用extern,如果定义在引用函数之后,则需要用extern
tips
- 不要使用全局变量来在函数间传递参数和结果
- 尽量避免使用全局变量
丰田汽车的案子 - 使用全局变量和静态本地变量的函数时线程不安全的
static静态本地变量
-
在本地变量定义时加上
static
修饰符就成为静态本地变量 -
当函数离开的时候, 静态本地变量会继续存在并保持其值
-
静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
-
静态本地变量实际上时特殊的全局变量。
它们位于相同的内存区域 -
静态本地变量具有全局的生存期,函数内的局部作用域。
static
在这里的意思是局部作用域(本地可访问) -
在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
-
在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
register寄存器变量
- 只有局部自动变量和形式参数可以作为寄存器变量,而全局变量和局部静态变量static不行。
- 因为register变量可能不存放在内存中,所以不能用“
&
”来获取register变量的地址
16,函数
不对外公开的函数
函数的指针
void f(void)
{
printf("hello\n");
}
void (*pf)(void) = f;
函数的指针数组
void f(int i)
{
printf("in f(), %d\n", i);
}
void g(int i)
{
printf("in g(), %d\n", i);
}
void h(int i)
{
printf("in h(), %d\n", i);
}
void (*fa[])(int) = {f,g,h};
求数组大小:
sizeof(fa)/sizeof(fa[0])
函数做参数传给另一个函数(指针)
#include <stdio.h>
int plus(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
}
void cal(int (*f)(int, int))
{
printf("%d\n", (*f)(2,3));
}
int main(void)
{
cal(plus);
cal(minus);
return 0;
}
返回指针的函数
- 返回本地变量的地址是危险的
- 返回全局变量或者静态本地变量的地址是安全的
- 返回在函数内
malloc
的内存是安全的,但是容易造成问题 - 最好的做法是返回传入的指针
17,编译预处理指令
#
开头的时编译预处理指令
它们不是C语言的成分,但是C语言程序离不开它们#define
用来定义一个宏
#define
#define
<名字> <值>- 注意没有结尾的分号,因为不是C的语句
- 名字必须是一个单词,值可以是各种东西
- 在C语言的编译器开始编译之前,编译预处理程序会把程序中的名字换成值(完全的文本替换)
- gcc
--save-temps
tail xxx.i
,查看预处理后的文件
宏
- 如果一个宏的值中有其他的宏的名字,也是会被替换的
- 如果一个宏的值超过一行,最后一行之前的行末需要加
\
- 宏的值后面出现的注释不会被当作宏的值的一部分
- 宏的结尾不要加分号
因为实际引用宏的时候都会在结尾加分号,定义时加分号就会导致有两个分号,这会破坏原本程序的逻辑结构(如if
和else
之间)
没有值的宏
#define _DEBUG
这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了。
带参数的宏
( 像函数的宏)
- 宏可以带参数
#define cube(x) ((x)*(x)*(x))
- 可以带多个参数
#define MIN(a,b) ((a)>(b) ? (b):(a))
- 也可以组合(嵌套)使用其他宏
带参数的宏的原则
一切都要有括号
- 整个值要括号
- 参数出现的每个地方都要括号
错误的宏定义
#define RADTODEG(x) (x*57.29578)
#define RADTODEG(x) (x) * 57.29578
正确的定义
- 整个值要括号
- 参数出现的每个地方都要括号
#define RADTODEG(x) ((x) * 57.29578)
预定义的宏
__LINE__
:源代码文件当前所在的行号
__FILE__
:源代码文件的文件名
__DATE__
:编译时的日期
__TIME__
:编译时的时间
__STDC__
宏的缺点:
- 没有类型检查
- 占用空间比较多
其他编译预处理指令
- 条件编译
- error
18,大程序结构
多个.C
文件
- main()里的代码太长了适合分成几个函数
- 一个源代码文件太长了适合分成几个文件
- 两个独立的源代码文件不能编译形成可执行的程序
编译单元
- 一个
.C
文件时一个编译单元 - 编译器每次编译只处理一个编译单元
项目
- 在Dev C++中新建一个项目,然后把几个源代码文件加入进去
- 对于项目,Dev C++的编译会把一个项目中所以的源代码文件都编译后,链接起来
- 有的IDE有分开的编译和构建两个按钮,前者时对单个源代码文件编译,后者是对整个项目做链接
头文件
- 只有声明可以被放在头文件中
否则会造成一个项目中有多个编译单元里有重名的实体
(某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在) - 把函数原型放到一个头文件(以
.h
结尾)中,在需要调用这个函数的源代码文件(.c
文件)中#include
这个头文件,就能让编译器在编译的时候就知道函数原型。(类型检查) - 在使用和定义这个函数的地方都应该#include这个头文件
- 一般的做法就是任何
.c
都有对应的同名的.h
,把所有对外公开的函数的原型和全局变量的声明都放进去
#include
#include
是一个编译预处理指令,和宏一样,在编译之前就处理了- 它把那个文件的全部文本内容原封不动地插入到它所在的地方
- 所以也不是一定要在
.c
文件的最前面#include
" "
和 < >
的两种插入文件形式
" "
要求编译器首先在当前目录(.C
文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找<>
让编译器只在指定的目录去找- 编译器自己知道自己的标准库的头文件在哪里
- 环境变量和编译器命令行参数也可以指定寻找头文件的目录
#include的误区
- #include不是用来引入库的,只是把后面文件内容原封不动插入进来
stdio.h
里只有printf
的原型,printf的代码在另外的地方,
某个.lib
(Windows)或.a
(Unix)中- 现在的C语言编译器默认会引入所有的标准库
- #include <stdio.h> 只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值时正确的类型
19,声明和定义
- 声明是不产生代码的东西
- 函数原型
- 变量声明
- 结构声明
- 宏声明
- 枚举声明
- 类型声明
- inline函数
- 定义是产生代码的东西
变量的声明和定义
int i;
是变量的定义
extern int i;
是变量的声明
重复声明
- 同意个编译单元里,同名的结构不能被重复声明
- 如果你的头文件里有结构的声明,很难保证这个头文件不会在一个编译单元里被#include多次
- 所以需要“标准头文件结构”
标准头文件结构
- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
- #pragma once也能起到相同的作用,但是不是所有的编译器都支持
#ifndef __LIST_HEAD__
#define __LIST_HEAD__
#include "node.h"
typedef struct _list{
Node* head;
Node* tail;
} List;
#endif
20,格式化的输入输出
- double型,printf()用%f输出,而scanf用%lf来接受输入。
printf
%[flags][width][.prec][hlL]type
flag | 含义 |
---|---|
- | 左对齐 |
+ | 在前面放+或- |
(space) | 正数留空 |
0 | 0填充 |
width或prec | 含义 |
---|---|
number | 最小字符数(包括小数部分) |
* | 下一个参数是字符数 |
.number | 小数点后面的位数 |
.* | 下一个参数是小数点后的位数 |
类型修饰hlL | 含义 |
---|---|
hh | 单个字节 |
h | short |
l | long |
ll | longlong |
L | long double |
type | 用于 |
---|---|
i 或 d | int |
u | unsigned int |
o | 八进制 |
x | 十六进制 |
X | 字母大写的十六进制 |
f 或 F | float,6 |
e 或 E | 指数 |
g | float |
G | float |
a 或 A | 十六进制浮点 |
c | char |
s | 字符串 |
p | 指针 |
n | 读入/写出的个数 |
scanf
%[flag]type
flag | 含义 |
---|---|
* | 跳过 |
数字 | 最大字符数 |
hh | char |
h | short |
l | long,double |
ll | long long |
L | long double |
type | 用于 |
---|---|
d | int |
i | 整数,可能为十六进制或八进制 |
u | unsigned int |
o | 八进制 |
x | 十六进制 |
a,e,f,g | float |
c | char |
s | 字符串(单词) |
[…] | 所允许的字符 |
p | 指针 |
[^.]
- GPS模块产生的数据
//$GPRMC,004319.00,A,3016.98468,N,12006.39211,E,0.047,,130909,,,D*79
- 用scanf来读取数据
scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", sTime,sAV,sLati,&sNW,sLong,&sEW,sSpeed,sAngle,sDate);
连续使用两个scanf(或getchar)输入字符误读问题
-
在C语言中,如果使用字符型变量(就是char型)时在有连续输入的情况下,很容易因为出现垃圾字符而导致程序的流程非法。
-
所以第二个scanf(或getchar)可能会误读字符(如:回车符)
-
解决办法:
1,把第一个scanf 改成scanf ("%c\n", &c1);
(小细节:在回车符后面有其他字符才可以读取成功)
2,在第二个scanf (或getchar)前面添加语句:while (getchar () != '\n');
3,在第一个scanf后加getchar()
,直接将'\n'
取出
4,在第一个scanf (或getchar)后添加下面的语句fflush (stdin);
清除输入缓冲区(对输入流的操作未定义,慎用;GCC不支持)
scanf和printf的返回值
- 读入的项目数
- 输出的字符数
在要求严格的程序中应该判断每次调用sanf或printf的返回值, 从而了解程序运行中是否存在问题
21,文件输入输出
用>
和 <
做重定向
>
输出结果到文件中
<
从文件中获取输入
FILE
在头文件<stdio.h>里已经声明好了FILE类型
FILE* fopen(const char * restrict path, const char * restrict more);
int fclose(FILE *stream);
fscanf(FILE*, ...)
fprintf(FILE*, ...)
打开文件的标准代码
FILE* fp = fopen(“file”, “r”);
if(fp)
{
fscanf(fp, …);
fclose(fp);
}
else
{
…
}
fopen
- 第一参数:文件名(字符串)
- 第二参数:操作模式(字符串)
模式参数 | 含义 |
---|---|
r | 打开只读 |
r+ | 打开读写,从文件头开始 |
w | 打开只写。如果不存在则新建,如果存在则清空 |
w+ | 打开读写。 如果不存在则新建,如果存在则清空 |
a | 打开追加。如果不存在则新建,如果存在则从文件尾开始 |
…x | 只新建,如果文件已存在则不能打开 |
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE *fp = fopen("12.in", "r");
if(fp)
{
int num;
fscanf(fp, "%d", &num);
printf("%d\n", num);
fclose(fp);
}
else
{
printf("无法打开文件\n");
}
return 0;
}
22,文件(二进制、文本)
- 其实所有的文件最终都是二进制的
- 文本文件无非是用最简单的方式可以读写的文件
more、tail
cat
vi
- 而二进制文件是需要专门的程序来读写的文件
- 文本文件的输入输出是格式化,可能经过转码
文本VS二进制
二者可移植性
程序为什么要文件
二进制读写
为什么需要nitems
- 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的
- 一个结构的大小是
size
- 于是
nitem
就是用来说明这次读写几个结构变量
// student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
#define STR_LEN 20
typedef struct _student {
char name[STR_LEN];
int gender;
int age;
} Student;
#endif
//student.c
#include <stdio.h>
#include "student.h"
void getList(Student aStu[], int number);
int save(Student aStu[], int number);
int main(int argc, char const *argv[])
{
int number = 0;
printf("请输入学生数量:");
scanf("%d", &number);
Student aStu[number];
getList(aStu, number);
if(save(aStu, number))
{
printf("保存成功\n");
}
else
{
printf("保存失败\n");
}
return 0;
}
void getList(Student aStu[], int number)
{
char format[STR_LEN];
sprintf(format, "%%%ds",STR_LEN-1);
int i;
for(i=0; i<number; i++)
{
printf("第%d个学生:\n", i);
printf("\t姓名:");
scanf(format, aStu[i].name);
printf("\t性别(0-男,1-女,2-其他):");
scanf("%d", &aStu[i].gender);
printf("\t年龄:");
scanf("%d", &aStu[i].age);
}
}
int save(Student aStu[], int number)
{
int ret = -1;
FILE *fp = fopen("student.data", "w");
if(fp)
{
ret = fwrite(aStu, sizeof(Student), number, fp);
fclose(fp);
}
return ret == number;
}
在文件中定位
// student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
#define STR_LEN 20
typedef struct _student {
char name[STR_LEN];
int gender;
int age;
} Student;
#endif
//student2.c
#include <stdio.h>
#include "student.h"
void read(FILE *fp, int index);
int main(int argc, char const *argv[])
{
FILE *fp = fopen("student.data", "r");
if(fp)
{
fseek(fp, 0L, SEEK_END); //移到末尾
long size = ftell(fp); //末尾的位置表达的就是文件的大小
int number = size / sizeof(Student);
int index = 0;
printf("有%d个数据, 你要看第几个:", number);
scanf("%d", &index);
read(fp, index-1);
fclose(fp);
}
return 0;
}
void read(FILE *fp, int index)
{
fseek(fp, index * sizeof(Student), SEEK_SET);
Student stu;
if(fread(&stu, sizeof(Student), 1, fp) == 1)
{
printf("第%d个学生:\n", index+1);
printf("\t姓名:%s\n", stu.name);
printf("\t性别:");
switch(stu.gender) {
case 0: printf("男\n"); break;
case 1: printf("女\n"); break;
case 2: printf("其他\n"); break;
}
printf("\t年龄:%d\n", stu.age);
}
}
输出一个数的二进制
- 置掩码mask最高位为1,其余为0
- 逐步右移,按位与,求目标数每一个二进制位的值
#include <stdio.h>
int main(int argc, char const *argv[])
{
int number;
scanf("%d", &number);
// number = 12345;
unsigned mask = 1u<<31; //置mask最高位为1,其余为0
for( ; mask; mask>>=1) //逐步右移,按位与,求number每一位的值
{
printf("%d", number & mask? 1:0);
}
printf("\n");
return 0;
}
位运算
- C语言按位运算的运算符
逻辑运算VS按位运算
23,单片机MCU的SFR
U0LCR --> 0xE000C00C
控制单比特bit的操作
const unsigned int SBS = 1u<<2;
const unsigned int PE = 1u<<3;
U0LCR |= SBS | PE;
U0LCR &= ~SBS;
U0LCR &= ~PE;
U0LCR &= ~(SBS | PE);
控制多个比特位的操作
位段
- 可以直接用位段的成员名称来访问
比移位、与、或还方便 - 编译器会安排其中的位的排列(从左或右开始排), 不具有可移植性
- 当所需的位超过一个int时会采用多个int
- 把一个int的若干位组合成一个结构
struct {
unsigned int leading : 3; //数字表示占用比特bit数
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 11;
};
#include <stdio.h>
void prtBin(unsigned int number);
struct U0 {
unsigned int leading : 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 32;
};
int main(int argc, char const *argv[])
{
struct U0 uu;
uu.leading = 2;
uu.FLAG1 = 0;
uu.FLAG2 = 1;
uu.trailing = 0;
printf("sizeof(uu)=%lu\n", sizeof(uu));
prtBin(*(int*)&uu);
}
void prtBin(unsigned int number)
{
unsigned mask = 1u<<31;
for( ; mask; mask>>=1)
{
printf("%d", number & mask? 1:0);
}
printf("\n");
}
24,数的表示
浮点数
无符号数与带符号数
- 常数默认是带符号数,如果有
U
作后缀则是无符号数,如23U
- 如果两者混合使用,则带符号数默认转换为无符号数(包括比较操作符)
- 无符号数应用场景:
模运算
按位运算 - 不能仅因为取值范围是非负而使用
实例一:会无限循环,因为无符号数没有负数
unsigned i;
for(i = cnt-2; i>=0; i--)
{
a[i] += a[i+1];
}
实例二:也是无限循环, sizeof返回类型是unsigned
#define DELTA sizeof(int)
int i;
for(i = CNT; i-DELTA >= 0; i-= DELTA)
...
- 不同数据类型混合使用时,会向着精度更高、长度更长的方向转换
char-->short-->int-->float-->double
,signed-->unsigned