指针
简介
指针是什么? 首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。
这里对于我们有JS经验的人来说,应该不难理解。就跟JS中的复杂数据类型一样, 比如声明一个数组时,其实执行了两件事,一是在堆中存储了数据,二是声明了一个变量指向这个堆中的数据,这个变量就是指针。
字符表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char表示一个指向字符的指针,float*表示一个指向float类型的值的指针。
int* intPtr;
上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。 星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。
int *intPtr;
int * intPtr;
int* intPtr;
这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。
// 正确
int * foo, * bar;
// 错误
int* foo, bar;
上面示例中,第二行的执行结果是,foo是整数指针变量,而bar是整数变量,即*只对第一个变量生效。 一个指针指向的可能还是指针,这时就要用两个星号**表示。
int** foo;
上面示例表示变量foo是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。
* 运算符
*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。
void increment(int* p) {
*p = *p + 1;
}
上面示例中,函数increment()的参数是一个整数指针p。函数体里面,p就表示指针p所指向的那个值。对p赋值,就表示改变指针所指向的那个地址里面的值。
上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。 事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。
变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。
& 运算符
&运算符用来取出一个变量所在的内存地址。
int x = 1;
printf("x's address is %p\n", &x);
上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址。
上一小节中,参数变量加1的函数,可以像下面这样使用。
void increment(int* p) {
*p = *p + 1;
}
int x = 1;
increment(&x);
printf("%d\n", x); // 2
上面示例中,调用increment()函数以后,变量x的值就增加了1,原因就在于传入函数的是变量x的地址&x。
&运算符与*运算符互为逆运算,下面的表达式总是成立。
int i = 5;
if (i == *(&i)) // 正确
函数
简介
和js中的函数相比较,C语言中的函数存在以下几点不同
- 不需要function关键字声明
- 函数调用时,参数个数必须与定义里面的参数个数一致
- 函数必须先声明后调用(不存在hoist)
- 实参和形参的类型不一致时,会自动转换为形参类型
- 如果没有写返回值类型,默认是int
int plus_one(int n) {
return n + 1;
}
上面的代码声明了一个函数plus_one()。
函数调用时,参数个数必须与定义里面的参数个数一致,参数过多或过少都会报错。
int plus_one(int n) {
return n + 1;
}
plus_one(2, 2); // 报错
plus_one(); // 报错
上面示例中,函数plus_one()只能接受一个参数,传入两个参数或不传参数,都会报错。
int a = plus_one(13);
int plus_one(int n) {
return n + 1;
}
上面示例中,在调用plus_one()之后,才声明这个函数,编译就会报错。
#include <stdio.h>
int main(void)
{
void change2(double num1, double num2)
{
printf("num1 = %.1f\n", num1);
printf("num2 = %f", num2);
}
change2(1.2, 3);
}
上面示例中,自动将实参转为double后保存
main方法
C 语言规定,main()是程序的入口函数,即所有的程序一定要包含一个main()函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。其他函数都是通过它引入程序的。
main()的写法与其他函数一样,要给出返回值的类型和参数的类型,就像下面这样。
int main(void) {
printf("Hello World\n");
return 0;
}
上面示例中,最后的return 0;表示函数结束运行,返回0。
C 语言约定,返回值0表示函数运行成功,如果返回其他非零整数,就表示运行失败,代码出了问题。系统根据main()的返回值,作为整个程序的返回值,确定程序是否运行成功。
正常情况下,如果main()里面省略return 0这一行,编译器会自动加上,即main()的默认返回值为0。所以,写成下面这样,效果完全一样。
int main(void) {
printf("Hello World\n");
}
由于 C 语言只会对main()函数默认添加返回值,对其他函数不会这样做,所以建议总是保留return语句,以便形成统一的代码风格。
系统在程序启动调用main函数会传入以下两个参数
- 参数个数
- 每个参数组成的数组,第一个参数总是main函数执行文件的路径,剩余参数皆为执行时传入的参数
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("argc = %d\n", argc);
printf("argv [0] pwd= %s\n", argv[0]);
printf("argv [1] args%s\n", argv[1]);
};
函数原型
前面说过,函数必须先声明,后使用。由于程序总是先运行main()函数,导致所有其他函数都必须在main()函数之前声明。
但是,main()是整个程序的入口,也是主要逻辑,放在最前面比较好。另一方面,对于函数较多的程序,保证每个函数的顺序正确,会变得很麻烦。
C 语言提供的解决方法是,只要在程序开头处给出函数原型,函数就可以先使用、后声明。所谓函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。
int twice(int);
int main(int num) {
return twice(num);
}
int twice(int num) {
return 2 * num;
}
上面示例中,函数twice()的实现是放在main()后面,但是代码头部先给出了函数原型,所以可以正确编译。只要提前给出函数原型,函数具体的实现放在哪里,就不重要了。
函数原型包括参数名也可以,虽然这样对于编译器是多余的,但是阅读代码的时候,可能有助于理解函数的意图。
int twice(int);
// 等同于
int twice(int num);
上面示例中,twice函数的参数名num,无论是否出现在原型里面,都是可以的。
注意,函数原型必须以分号结尾。
一般来说,每个源码文件的头部,都会给出当前脚本使用的所有函数的原型。
exit
exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。
exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。
// 程序运行成功
// 等同于 exit(0);
exit(EXIT_SUCCESS);
// 程序异常中止
// 等同于 exit(1);
exit(EXIT_FAILURE);
在main()函数里面,exit()等价于使用return语句。其他函数使用exit(),就是终止整个程序的运行,没有其他作用。
C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h。
int atexit(void (*func)(void));
atexit()的参数是一个函数指针。注意,它的参数函数(下例的print)不能接受参数,也不能有返回值。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
void print(void) { printf("somethig run while exit!"); };
atexit(print);
exit(EXIT_FAILURE);
}
上面示例中,exit()执行时会先自动调用atexit()注册的print()函数,然后再终止程序。
函数说明符
extern 说明符
对于多文件的项目,源码文件会用到其他文件声明的函数。这时,当前文件里面,需要给出外部函数的原型,并用extern说明该函数的定义来自其他文件。
extern int foo(int arg1, char arg2);
int main(void) {
int a = foo(2, 3);
// ...
return 0;
}
上面示例中,函数foo()定义在其他文件,extern告诉编译器当前文件不包含该函数的定义。
不过,由于函数原型默认就是extern,所以这里不加extern,效果是一样的。
static 说明符
默认情况下,每次调用函数时,函数的内部变量都会重新初始化,不会保留上一次运行的值。static说明符可以改变这种行为。
static用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。
#include <stdio.h>
void counter(void) {
static int count = 1; // 只初始化一次
printf("%d\n", count);
count++;
}
int main(void) {
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4
}
上面示例中,函数counter()的内部变量count,使用static说明符修饰,表明这个变量只初始化一次,以后每次调用时都会使用上一次的值,造成递增的效果。
注意,static修饰的变量初始化时,只能赋值为常量,不能赋值为变量。
int i = 3;
static int j = i; // 错误
上面示例中,j属于静态变量,初始化时不能赋值为另一个变量i。
另外,在块作用域中,static声明的变量有默认值0。
static int foo;
// 等同于
static int foo = 0;
static可以用来修饰函数本身。
static int Twice(int num) {
int result = num * 2;
return(result);
}
上面示例中,static关键字表示该函数只能在当前文件里使用,如果没有这个关键字,其他文件也可以使用这个函数(通过声明函数原型)。
static也可以用在参数里面,修饰参数数组。
int sum_array(int a[static 3], int n) {
// ...
}
上面示例中,static对程序行为不会有任何影响,只是用来告诉编译器,该数组长度至少为3,某些情况下可以加快程序运行速度。 另外,需要注意的是,对于多维数组的参数,static仅可用于第一维的说明。
const 说明符
函数参数里面的const说明符,表示函数内部不得修改该参数变量。
void f(int* p) {
// ...
}
上面示例中,函数f()的参数是一个指针p,函数内部可能会改掉它所指向的值*p,从而影响到函数外部。
为了避免这种情况,可以在声明函数时,在指针参数前面加上const说明符,告诉编译器,函数内部不能修改该参数所指向的值。
void f(const int* p) {
*p = 0; // 该行报错
}
上面示例中,声明函数时,const指定不能修改指针p指向的值,所以*p = 0就会报错。
但是上面这种写法,只限制修改p所指向的值,而p本身的地址是可以修改的。
void f(const int* p) {
int x = 13;
p = &x; // 允许修改
}
上面示例中,p本身是可以修改,const只限定*p不能修改。
如果想限制修改p,可以把const放在p前面。
void f(int* const p) {
int x = 13;
p = &x; // 该行报错
}
如果想同时限制修改p和*p,需要使用两个const。
void f(const int* const p) {
// ...
}
字符串
特征
C语言的字符串是以字符数组的形态存在的
C语言的字符串具有以下特征:
- 以0(整数0)结尾的一串字符
- *0 或 ‘\0’ 是一样的,但是和’0’不同
- 0 标志字符串的结束,但他不是字符串的一部分
- 计算字符串长度的时候不包含这个字符
#include <stdio.h>
int main(void)
{
char *str = "hello"; // 字符指针
char str2[] = {'a', 'b', '\0'};
char word[] = "hello";
char line[10] = "hello11";
}
注意: []中的长度是可以省略不写的; 采用第2种方式的时候最后一个元素必须是’0’,’0’表示字符串的结束标志; 采用第2种方式的时候在数组中不能写中文。 在输出字符串的时候要使用:printf(“%s”,字符数组名字);或者puts(字符数组名字);
字符串常量
#include <stdio.h>
int main(void)
{
char *str = "Hello World";
char *str1 = "Hello World1";
str[0] = 'S'; // 报错
printf("str pointer %p\n", str);
printf("str = %s\n", str);
printf("str1 pointer %p\n", str1);
}
其中
char *str = "Hello World";
- s是一个指针,初始化为指向一个字符串常量。
- 由于这个是常量所在的地方,所以实际上s是const char* s ,但由于历史原因,编译器接收不带const的写法
- 试图对s所指的字符串做写入会导致预期之外的后果。
- 且因为指向的这个字符串是只读的,所以声明相同的字符串常量是指向同一内存空间
所以尽量使用字符串数组
字符串的输入和输出
scanf输入字符串
#include <stdio.h>
int main(void)
{
char string[8];
char string1[8];
scanf("%s", string);
scanf("%7s", string1);
printf("%s##%s##\n", string, string1);
}
scanf读入一个单词(到空格、tab或回车为止)
putchar
int putchar(int c);
- 向标准输出写出一个字符
- 返回写了几个字符,EOF(-1)表示写失败
getchar
int getchar(void)
- 从标准输入得到一个字符
- 返回类型是int,是为了返回EOF(-1)
字符串的方法
strlen()
strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。
#include <string.h>
char* str = "hello";
int len = strlen(str); // 5
strcmp()
strcmp()函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h头文件里面。
int strcmp(const char* s1, const char* s2);
按照字典顺序,如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。
下面是一个用法示例。
// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays
strcmp(s1, s2) // 0
strcmp(s1, s3) // 大于 0
strcmp(s3, s1) // 小于 0
注意,strcmp()只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==)就能比较。所以,不要把字符类型(char)的值,放入strcmp()当作参数。
strcpy()
将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。
strcpy(char dest[], const char source[])
strchr()
char *strchr(const char *s,int c);
char *strrchr(const char *s,int c);
// 返回NULL表示没有找到
char s[] = "hello";
char *p = strchr(s, 'l'); //llo
printf("%s", p);
返回满足该条件开始到结束的字符串
内存管理
进程空间
- 程序,是经过源码编译后的可执行文件,可执行文件可以多次被执行,比如我可以多次打开谷歌浏览器
- 进程,是程序加载到内存后开始执行到执行结束,这样一段时间概念,多次打开谷歌浏览器,每打开一次都是一个进程,每当关闭一个浏览器,则表示该进程结束
- 程序是静态概念,而进程是动态/时间的概念
进程空间示意图 有个进程和空间的概念,程序被加载到内存后内存的空间布局是怎么样的:
struct结构
简介
C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。
实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。
- 复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。
- 某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传入。
为了解决这些问题,C 语言提供了struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起。 这样不仅为编程提供方便,也有利于增强代码的可读性。 C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。
下面是struct自定义数据类型的一个例子。
struct fraction {
int numerator;
int denominator;
};
struct fraction f1;
f1.numerator = 22;
f1.denominator = 7;
除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。
struct car {
char* name;
float price;
int speed;
};
struct car saturn = {"Saturn SL/2", 16000.99, 175};
上面示例中,变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。
注意,大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。
struct car saturn = {.speed=172, .name="Saturn SL/2"};
上面示例中,初始化的属性少于声明时的属性,这时剩下的那些属性都会初始化为0。
struct 的复制
struct 变量可以使用赋值运算符(=),复制给另一个变量,这时会生成一个全新的副本。 系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。 这一点跟数组的复制不一样,务必小心。
struct cat { char name[30]; short age; } a, b;
strcpy(a.name, "Hula");
a.age = 3;
b = a;
b.name[0] = 'M';
printf("%s\n", a.name); // Hula
printf("%s\n", b.name); // Mula
struct 指针
#include <stdio.h>
struct turtle {
char* name;
char* species;
int age;
};
void happy(struct turtle t) {
t.age = t.age + 1;
}
int main() {
struct turtle myTurtle = {"MyTurtle", "sea turtle", 99};
happy(myTurtle);
printf("Age is %i\n", myTurtle.age); // 输出 99
return 0;
}
上面示例中,函数happy()传入的是一个 struct 变量myTurtle,函数内部有一个自增操作。但是,执行完happy()以后,函数外部的age属性值根本没变。 原因就是函数内部得到的是 struct 变量的副本,改变副本影响不到函数外部的原始数据。
通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。 这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。
struct 指针传入函数的写法如下:
void happy(struct turtle* t) {
}
happy(&myTurtle);
上面代码中,t是 struct 结构的指针,调用函数时传入的是指针。struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成&myTurtle。
函数内部也必须使用(*t).age的写法,从指针拿到 struct 结构本身。
void happy(struct turtle* t) {
(*t).age = (*t).age + 1;
}
上面示例中,(t).age不能写成t.age,因为点运算符.的优先级高于*。 *t.age这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果。
(*t).age这样的写法很麻烦。C 语言就引入了一个新的箭头运算符(->), 可以从 struct 指针上直接获取属性,大大增强了代码的可读性
void happy(struct turtle* t) {
t->age = t->age + 1;
}
// ptr == &myStruct
myStruct.prop == (*ptr).prop == ptr->prop
自定义数据类型 typedef
简介
typedef命令用来为某个类型起别名。
typedef type name;
上面代码中,type代表类型名,name代表别名。
typedef unsigned char BYTE;
BYTE c = 'z';
联合 union
- 在联合类型中,所有成员都是共用同一块内存空间
- 同一时间只有一个成员是有效的
- union的大小是其最大的成员
#include <stdio.h>
int main(void)
{
typedef union
{
int age;
char capital;
} Person;
Person p;
p.age = 1;
p.capital = 'a';
printf("%c", p.age); // 'a'
}
全局变量和静态本地变量
全局变量的特点
- 定义在main函数外面的变量
- 全局变量具有全局的生存期和作用域
- 他们与任何函数都无关,在任何函数内都可以使用它。
- 没有做初始化的变量会得到0。(本地变量未初始化则是一串随机的内存空间)
- 没有做初始化的指针会得到NULL。
int all;
int main(void){
printf("%d",all); // 0
}
静态本地变量和全局变量的比较
- 静态本地变量其实是特殊的全局变量
- 他们位于相同的内存区域
- 静态本地变量具有全局的生存期,函数内部的作用域
- static 在这里的意思是局部作用域(本地可访问)
宏
编译预处理指令
编译之前会进行处理的指令
- #开头的是编译预处理指令
- 它们不是C语言的成分,但C语言离不开它
- #define用来定义一个宏
宏的定义
现在我们会如何表示一个不可变的值呢?
const double PI = 3.1415926;
这是在C99之后才支持的const的写法,在此之前常用#define定义一个宏
#define
- #define <名字><值>值>名字>
- 注意没有结尾的分号,因为不是C语言的语句
- 名称必须是一个单词,值可以是各种东西
- 在C语言的编译器在开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值。
- 完全本的文本替换
- gcc –save-temps可以保存预编译的产物
- 如果一个宏中含有其他宏的名字,也是会被替换的
- 如果一个宏的值超过一行,最后一行之前的行末需要加\
- 定义宏中的注释不会被当作宏的一部分解析
#include <stdio.h>
#define PI 3.13159
#define PI2 2 * PI // PI*2
#define FORMAT "%f\n"
#define print \
printf("PI=%f\n", PI); \
printf("PI2=%f\n", PI2);
int main(void)
{
printf(FORMAT, PI);
printf(FORMAT, PI2);
print;
}
没有值的宏
- #define _DEBUG
- 这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了。
预定义的宏
__LINE__ 当前行号
__FILE__ 文件名
__DATE__ 当前日期
__TIME__ 当前时间
带参数的宏
定义的原则:
- 一切都要括号
- 整个值都要括号
- 参数出现的每个地方都要括号
#define RADTODEG(x)((x)*57.2233)