1、变量
extern int a; // 声明一个全局变量 a
int a; // 定义一个全局变量 a
extern int a = 0; // 定义一个全局变量 a 并给初值。一旦给予赋值,一定是定义,定义才会分配存储空间
int a = 0; //定义一个全局变量 a,并给初值
声明之后你不能直接使用这个变量,需要定义之后才能使用。
第四个等于第三个,都是定义一个可以被外部使用的全局变量,并给初值。
糊涂了吧,他们看上去可真像。但是定义只能出现在一处。也就是说,不管是 int a 还是 int a=0 都只能出现一次,而那个 extern int a 可以出现很多次。
当你要引用一个全局变量的时候,你就要声明 extern int a 这时候 extern 不能省略,因为省略了,就变成 int a 这是一个定义,不是声明。
const int a; a = 10; //报错
const int b = 0; // 正常
const 定义的是变量不是常量,只是这个变量的值不允许改变是常变量,是只读类型的变量,编译运行的时候起作用存在类型检查。define是预处理阶段的简单的字符替换。
2、static和extern
static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。任何跟 static 变量或方法同一个文件中函数或方法都可以引用。
/* 函数声明 */
void func1(void);
static int count=10; /* 全局变量 - static 是默认的 */
int main()
{
while (count--) {
func1();
}
return 0;
}
void func1(void)
{
// 'thingy' 是 'func1' 的局部变量 - 只初始化一次,每次调用函数 'func1' 'thingy' 值不会被重置。
static int thingy=5;
thingy++;
printf(" thingy 为 %d , count 为 %d\n", thingy, count);
}
当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
// main.c
int count ;
extern void write_extern();
int main()
{
count = 5;
write_extern();
}
// test.c
extern int count;
void write_extern(void)
{
printf("count is %d\n", count);
}
如果我想引用一个全局变量或函数a,我只要直接在源文件中包含#include (xxx.h包含了a的声明)不就可以了么,为什么还要用extern呢?
include导入的是整个文件,可能会造成重复定义,extern是更精细化的include
3、数组
指针:也是一个变量,存储的数据是地址。数组名:代表的是该数组最开始的一个元素的地址。区别:指针是一个变量,可以进行数值运算。数组名不是变量,不可以进行数值运算。
对数组来说,array和&array是等价的
int array[10]; printf("%p %p %p\n",array, &array,&(array[0])); //0x16fdff498 0x16fdff498 0x16fdff498
char *p=(char*)&array == char *p=array ;
想知道数组元素个数可以使用 sizeof(a)/sizeof(a[0]),但是数组类型做函数的参数的时候不能在函数内部用此方法获取元素个数,要另加参数len把个数传进来。因为数组做函数参数时是被当做指针传进来的。
int a[] = {1,2,3,4,5};
void test2(int a[]){
int b = sizeof(a)/sizeof(a[0]);
printf("数组元素个数为:%d\n",b); //结果为2=sizeof(int *)/sizeof(int)=8/4
}
数组指针运算:把数组当成指针,就变成指针运算了。
int a[2] = {1,2};
printf("a = %d\n",a[0]);
printf("*(a+0) = %d\n",*(a + 0));
printf("a[1] = %d\n",a[1]);
printf("*a = %d\n",*a);
printf("*(a+1) = %d\n",*(a + 1));
printf("\n");
printf("a 的地址:%p\n",a); //0x16fdff4c0
printf("(a+0)的地址:%p\n",(a + 0)); //0x16fdff4c0
printf("(a+1)的地址:%p\n",(a + 1)); //0x16fdff4c4
4、指针
指针运算有:指针加减、指针比较。数组可以看成一个指针常量,不能递增,我们可以使用指针代替数组,因为变量指针可以递增。
int var[] = {10, 100, 200};
int i, *ptr;
ptr = var; //数组变为指针方便进行指针操作
for ( i = 0; i < 3; i++) // 通过count遍历,也可通过ptr比较地址大小进行遍历
{
printf("var[%d]:存储地址 = %p,存储值 = %d\n", i, ptr,*ptr);
ptr++;
}
函数指针和指针函数等以此类推
指针做参数:传递指针给函数,可直接对地址赋值。避免了返回值或数据修改的同步,但是更发散了
void getSeconds(unsigned long *par)
{
*par = time(NULL);//获取当前的秒数
}
unsigned long sec;
getSeconds(&sec);
printf("Number of seconds: %ld\n", sec );
函数与指针的复杂声明:
int *f(); // f是一个函数,返回一个指向int类型的指针
int (*pf)(); // pf是一个函数指针,返回一个int类型的对象
// void*是通用指针类型,任何类型的指针都可以转换为void*,并且从void*反转回来时不会丢失信息。
void *(*malloc_fn)(size_t sz); //malloc_fn指向一个函数,函数类型是void *()(size_t)
void (*free_fn) (void *ptr); //free_fn指向一个函数,函数类型是void ()(void *)
//*用来声明指针,()用来声明函数指针,[]用来声明数组。优先级是()>[]>*
int (*(*x)(int *,char *))(int);
首先找到x,然后根据优先级找到*x,则证明整条语句声明了一个指针。再继续看到(*x)右边是一个()符,则证明x是一个指向函数的指针,既然是函数指针那剩下的部分就是描述函数返回值类型和参数类型;再看()里则可以解读出函数的输入是一个整型指针和一个字符指针。然后再以“(*x)(int *,char *)”为整体看,找到优先级最高的是*符号,则证明返回值是一个指针;再继续看找到了(int)则证明这个指针是指向一个函数的,函数的参数是整型;再继续找到最后一个int,则看出这个函数的返回值是一个整型。到此解读完毕。
简要的说就是:x指向一个函数,函数类型是入参为(int *,char *),出参为int (*)(int)
int *(*(*fp)(int)) [10];
fp指向一个函数,函数类型是入参为(int),出参为int *(*) [10]
int p; -- 这是一个普通的整型变量
int *p; -- 首先从 p 处开始,先与 * 结合,所以说明 p 是一个指针, 然后再与 int 结合, 说明指针所指向的内容的类型为 int 型。所以 p 是一个返回整型数据的指针。
int p[3] -- 首先从 p 处开始,先与 [] 结合,说明 p 是一个数组, 然后与 int 结合, 说明数组里的元素是整型的, 所以 p 是一个由整型数据组成的数组。
int *p[3]; -- 首先从 p 处开始, 先与 [] 结合,因为其优先级比 * 高,所以 p 是一个数组, 然后再与 * 结合, 说明数组里的元素是指针类型, 然后再与 int 结合, 说明指针所指向的内容的类型是整型的, 所以 p 是一个由返回整型数据的指针所组成的数组。
int (*p)[3]; -- 首先从 p 处开始, 先与 * 结合,说明 p 是一个指针然后再与 [] 结合(与"()"这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组, 然后再与int 结合, 说明数组里的元素是整型的。所以 p 是一个指向由整型数据组成的数组的指针。
int **p; -- 首先从 p 开始, 先与 * 结合, 说是 p 是一个指针, 然后再与 * 结合, 说明指针所指向的元素是指针, 然后再与 int 结合, 说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中, 所以后面更复杂的类型我们就不考虑多级指针了, 最多只考虑一级指针。
int p(int); -- 从 p 处起,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里分析, 说明该函数有一个整型变量的参数, 然后再与外面的 int 结合, 说明函数的返回值是一个整型数据。
int (*p)(int); -- 从 p 处开始, 先与指针结合, 说明 p 是一个指针, 然后与()结合, 说明指针指向的是一个函数, 然后再与()里的 int 结合, 说明函数有一个int 型的参数, 再与最外层的 int 结合, 说明函数的返回类型是整型, 所以 p 是一个指向有一个整型参数且返回类型为整型的函数的指针。
int *(*p(int))[3]; -- 可以先跳过, 不看这个类型, 过于复杂从 p 开始,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里面, 与 int 结合, 说明函数有一个整型变量参数, 然后再与外面的 * 结合, 说明函数返回的是一个指针, 然后到最外面一层, 先与[]结合, 说明返回的指针指向的是一个数组, 然后再与 * 结合, 说明数组里的元素是指针, 然后再与 int 结合, 说明指针指向的内容是整型数据。所以 p 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
详细可参考:C 指针详解
5、字符串
字符串实际上是使用空字符 \0 结尾的一维字符数组。
char *str1 = "asdfgh"; //len=6,size=8
char str2[] = "asdfgh"; //len=6,size=7
char str3[8] = {'a', 's', 'd'}; //len=3,size=8
char str4[] = "as\0df"; //len=2,size=6
6、结构体
结构体声明在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
访问结构体变量用.
运算符,访问结构体指针用->
运算符。
// 结构体大小运算:结构体变量所占内存长度是其中最大成员大小的整数倍
struct A
{
char a1;
short int a2;
int a3;
double d;
};
struct B
{
long int b2;
short int b1;
struct A a;
};
// A=16,成员类型大小依次为1 2 4 8,以最大8位对齐字节,1 2 4占7个,补齐1个为8个,再加上8。
// B=8,去掉a算出其大小为4+2(+2)=8,直接加上A的大小=24。
7、共用体
特点:只有一个内存,是最大成员长度。值覆盖。
union Data
{
int i;
char str;
short d;
} data;
union Data1
{
int i;
float f;
char str[7];
short d;
} data1;
union Data2
{
int i;
float f;
char str[9];
short d;
} data2;
union Data3
{
int i;
float f;
char str[9];
double d;
} data3;
// 4 8 12 16
8、位域
针对整形int/unsigned int/signed int的,限制内存占用位数。如:开关变量中,我们只存储 0 或 1,只需要1位,这时可以使用位域以节省内存空间。赋值超限会警告,且值会异常(溢出)。
struct
{
unsigned int var;
} status1;
struct
{
unsigned int var1 : 1;
unsigned int var2 : 1;
...
unsigned int var32 : 1;
} status2;
// 同样4字节的空间,status1存储1个变量,status2能存储32个变量。
9、输入输出
#include <stdio.h>
//stdio.h是standard input output的缩写,常用printf() 和 scanf(),其他还有getchar/putchar、gets/puts。gets/puts方法会报安全警告,因为没有指定输入字符大小,会无限读取,一旦输入的字符大于数组长度,就会发生内存越界错误,可用fgets代替。
printf("%20.15f\n",1.0/3); //指定输出的数据占 20 列,其中包括 15 位小数
/*
short/int : %d
long: %ld (long 是 int 得修饰,不能算是一种单独的数据类型,只是比 int 多了四个字节的存储空间)
long long : %lld
char : %c
float/double : %f float 默认是 6 位小数输出;可以在 %f 中控制;例如:%.2f:输出两位小数。
char *s(字符串) :%s
unsigned: %u (signed:有符号类型, unsigned:无符号类型;默认都是有符号的)
八进制:%o 以 0 开头
十六进制:%x 以 0x 开头
*/
int a, b, c;int x = scanf("%d%d%d",&a,&b,&c);printf("%d\n%d\n",a,x);
//scanf() 函数有int返回值,错误时立刻返回 EOF,正确时返回接收到值的变量个数。
scanf("%d%d",&a,&b) //输入数字以空格分隔
scanf("c=%c",&c); // 必须输入c=3才能正常接收到值,格式和数据类型要一致
10、头文件
只引用一次头文件:如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
#ifndef FILENAME_H
#define FILENAME_H
the entire header file file
#endif
11、递归
注意:循环体、初始/终止条件
// 阶乘
unsigned int factorial(unsigned int i){
if (i <= 1) {
return 1;
}
return i * factorial(i-1);
}
// 斐波那契
unsigned int fibonaci(unsigned int i){
if (i <= 1) {
return i;
}
return fibonaci(i-1) + fibonaci(i-2);
}
12、可变参数
// 固定参数-地址访问
void fix_test(int a, double b, char c)
{
void *p = &a;//void * =&a 不需要转换但是使用时要转换
printf("%p %p %p\n", &a, &b, &c); //0x16fdff49c 0x16fdff490 0x16fdff48f
printf("%d %f %c\n", *((int*)p), *((double*)((int*)p-3)), *((char*)((char*)p-13)));
//在*((double*)((int*)p-3))中,double*是用来指明最外层运算符*的指针类型的,若缺省*运算符取到的值会是nil。int*是用来指明指针(地址)运算的指针类型的(int*)p-3也可以写成(char*)p-12
return;
}
// 可变参数-va_arg访问
double average(int num,...) {
va_list valist;
double sum = 0.0;
va_start(valist, num);
for (int i = 0; i < num; i++)
{
sum += va_arg(valist, int);
}
va_end(valist);
return sum / num;
}
// 可变参数-地址访问
// (3, 2, 3, 4);
void debug_arg_int(unsigned int num, ...){
char *p = #
printf("%d %d %d\n", *(p+4+8+8), *(p+20+8), *(p+28+8)); //*(p+20)的完整格式为*(int*)((char*)p+20)
// 打印地址0x16fdff48e中内容:x/32 0x16fdff48e
}
void debug_arg_short(unsigned short num, ...){
char *p = #
printf("%d %d %d\n", *(p+2+8+8), *(p+18+8), *(p+26+8));
}
13、内存管理
description = (char *)malloc( 200 * sizeof(char) );
description = (char *)calloc(200, sizeof(char));
description = (char *) realloc( description, 300 * sizeof(char) );
free(description);
// 直接使用原来的指针变量接收 realloc 的返回值是可能存在内存泄漏的。若 realloc 函数执行失败,description 原先所指向的空间不变,realloc 函数返回 NULL。此时 description 的值被赋为 NULL, 但原先指向的空间未被释放,造成了内存泄漏。
14、命令行参数
从外部(命令行)给程序传递参数控制程序
int main( int argc, char *argv[] )
//argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,如果传递了一个参数,argc 将被设置为 2
//两个参数名只是约定俗成,可以任意修改
getopt()用来分析命令行参数,详见:C语言之getopt函数
int getopt(int argc, char * const argv[], const char *optstring);
//前俩参数和main函数一样,optstring:选项字符串,一个字母表示不带值的参数,如果字母后带有一个:,表示必须带值的参数。如果带有两个:,表示是可选的参数。
//例如"ab:c::",程序运行时可接受的参数为"-a -b 1 -c"或者"-a -b 1 -c2"