1.第一个 C 程序
这里以 Visual Studio 2013 为例,打开 Visual Studio 2013 后,选择 文件 > 新建 > 项目,选择 Visual C++ > Win32 控制台应用程序,项目名为 Hello,然后点击 “确定” ,再点击 “下一步”,选择 “空项目”,然后点击完成。点击 视图 > 解决方案资源管理器,选中 源文件,右击选择 添加 > 新建项,输入名称 main.c,选择 添加,代码如下所示:
#include <stdio.h>
int main() {
printf("Hello World");
getchar();
return 0;
}
你可以直接点击 “本地 Windows 调试器” 运行程序,也可依次执行如下步骤:
- 1.右击项目并选择 “生成”,这将在项目所在的目录的 Debug 文件夹下生成可执行文件 Hello.exe。
- 2.在命令提示符中,切换到可执行文件所属的文件夹(通常是项目文件夹中的 Debug 文件夹)。
- 3.输入.\Hello.exe 运行它即可在控制台窗口上看到打印,打印如下:
Hello World
这里 #include <stdio.h> 是 C 语言中的一个预处理指令,它的作用是将标准输入输出库(Standard Input/Output Library)的头文件包含到你的程序中。没有这个头文件,你就无法使用如 printf()。getchar() 作用是让用户输入字符,在这里用于暂停程序的执行,否则控制台窗口打开后马上就消失了。
2. 指针
在 C/C++ 中万物皆指针,下面我们来学习指针的相关知识。修改代码如下:
#include <stdio.h>
int main() {
int number = 100;
printf("number的内存地址是:%p\n", &number); // %p 是地址输出的占位
getchar(); // 控制台等带用户输入
return 0;
}
运行后打印如下:
number的内存地址是:00CFFCCC
首先我们要明白,所有的变量都是存储在内存中,所有的变量都有对应的内存地址,我们可以通过这个内存地址拿到这个变量,反之,也可以通过这个变量拿到该变量的内存地址。
这里的 & 操作符是取地址符,用于获取 number 在内存中的地址,打印出来是一个十六进制的地址。
地址可以赋给指针,这样指针就指向了这个内存地址。修改代码如下:
#include <stdio.h>
int main() {
int number = 100;
printf("number的内存地址是:%p\n", &number);
int * a = &number;
printf("a的值为:%p\n", a);
getchar();
return 0;
}
打印如下:
number的内存地址是:00EFFB14
a的值为:00EFFB14
这里使用 int * a 定义了一个指针 a,然后把 number 的内存地址赋值给它, 通过打印可以看到 a 的值现在等于 number 的内存地址了。
再对代码进行修改,如下所示:
#include <stdio.h>
int main() {
int number = 100;
printf("number的内存地址是:%p\n", &number);
int * a = &number;
printf("a的值为:%p\n", a);
*a = 102;
printf("number的值为:%d\n", number);
getchar();
return 0;
}
打印如下:
number的内存地址是:00D6F8DC
a的值为:00D6F8DC
number的值为:102
注意第 8 行 * a 跟前面又不一样了,* 直接作用在变量 a 上,这里 a 就是一个内存地址,* a 表示取出这个内存地址所对应的值。也就是把 00D6F8DC 这个内存地址所对应的值改成了 102,也就等于把 number 的值改成了 102。
看下面的实例,使用指针实现两个变量的值的交换:
#include <stdio.h>
void changeAction(); // 由于 C 语言不支持函数重载,所以声明函数不需要写参数
int main() {
int a = 100;
int b = 200;
changeAction(&a, &b);
printf("交换后a=%d, b=%d\n", a, b);
getchar();
}
void changeAction(int * a, int * b) {
int temp = *a;
*a = *b;
*b = temp;
}
运行后打印如下:
交换后a=200, b=100
再来看一个实例:
#include <stdio.h>
void update(int m) {
printf(" update 函数中 m 的内存地址是:%p\n", &m);
}
int main() {
int m = 100;
update(m);
printf(" main 函数中 m 的内存地址是:%p\n", &m);
getchar();
return 0;
}
打印如下:
update 函数中 m 的内存地址是:00DDF6B8
main 函数中 m 的内存地址是:00DDF78C
两个函数中 m 的内存地址是不一样的,因为 update() 函数中 m 是新建的,它跟 main() 函数中的 m 值是一样的,但是是两个变量。
把上面的代码改一下:
#include <stdio.h>
void update(int * m) {
printf(" update 函数中 m 的内存地址是:%p\n", m);
}
int main() {
int m = 100;
update(&m);
printf(" main 函数中 m 的内存地址是:%p\n", &m);
getchar();
return 0;
}
打印出来两个内存地址就一样了,因为 update() 函数中的 m 实际是一个地址,它等于 main() 函数中的变量 m 的地址。
3. 数组
看下面的实例:
#include <stdio.h>
int main() {
// int [] arr = {1,2,3,4} 是错误的写法
int arr[] = { 1, 2, 3, 4 };
int i = 0;
for (i = 0; i < 4; ++i) {
printf("%d\n", arr[i]);
}
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("&arr[0] = %p\n", &arr[0]);
int * arr_p = arr;
printf("%d\n", *arr_p);
arr_p++;
printf("%d\n", *arr_p);
arr_p += 2;
printf("%d\n", *arr_p);
arr_p -= 3;
printf("%d\n", *arr_p);
arr_p += 2000;
printf("%d\n", *arr_p);
getchar();
return 0;
}
打印如下:
1
2
3
4
arr = 00AFF798
&arr = 00AFF798
&arr[0] = 00AFF798
1
2
4
1
0
可以看到 arr、&arr 和 &arr[0] 的值是一样的,事实上,这 3 个都可以用于表示该数组的内存地址。使用 int * arr_p = arr 将指针 arr_p 指向 arr 后,可以通过 *arr_p 拿到数组的第一个值 1,然后可以通过指针位移拿到其他的值。当位移超过了数组的范围的时候,拿到的是系统值,这里打印是 0,有些系统会打印一个无意义随机数,比如 547852156。
为什么可以通过指针位移来获取数组里面的每一个值呢,因为数组的内存是连续的,对于 int 数组,指针 + 1 表示指针往后挪动 4 个字节( 32 位系统)。
再来看一个例子:
#include <stdio.h>
int main() {
int num = 100;
int * int_p = #
double * double_p = #
long float * lf_p = #
printf("%d\n", sizeof int_p);
printf("%d\n", sizeof double_p);
printf("%d\n", sizeof lf_p);
getchar();
return 0;
}
打印如下:
4
4
4
从打印可以看到指针占用的内存大小都是 4 个字节( 32 位系统),那为什么定义指针的时候使用 int * 来定义指针,这里的 int 有什么作用呢?
答案是为了告诉编译器指针所指向的数据类型,int* 类型的指针,编译器解引用时会读取4字节(int 的大小),double* 类型的指针,编译器解引用时会读取 8 字节(double 的大小)。还有是为了方便指针算数运算,比如对于 int* 类型的指针,数组取值的时候,调用 arr_p++ 会让地址每次增加 sizeof(int)。
4.函数指针
#include <stdio.h>
void add(int num1, int num2); // 先声明,后面再实现
void mins(int num1, int num2) { // 声明和实现一起
printf("num1 - num2 = %d\n", (num1 - num2));
}
// 函数指针
void opreate(void(*method)(int, int), int num1, int num2) {
method(num1, num2);
printf(" opreate 函数的 method 指针是:%p\n", method);
}
int main() {
opreate(add, 1, 2);
printf(" main 函数的 add 指针是:%p\n", add);
printf("\n");
opreate(mins, 100, 10);
printf(" main 函数的 mins 指针是:%p\n", mins);
printf("\n");
printf("%p, %p\n", add, &add);
getchar();
return 0;
}
// 再实现
void add(int num1, int num2) {
printf("num1 + num2 = %d\n", (num1 + num2));
}
看 operate 函数的声明,是不是感觉函数指针用起来很像 Kotlin 里面的高阶函数,同样是把另一个函数作为参数传入,这样就可以在调用的时候再决定对 num1 和 num2 执行何种运算。
打印如下:
num1 + num2 = 3
opreate 函数的 method 指针是:000710E6
main 函数的 add 指针是:000710E6
num1 - num2 = 90
opreate 函数的 method 指针是:0007119F
main 函数的 mins 指针是:0007119F
000710E6, 000710E6
从打印可以看出来,在执行 opreate(add, 1, 2) 时, 函数指针 method 指向 add 函数,它们俩打印出来的地址是一样的,所以可以这样执行。
再来看一个例子:
#include <stdio.h>
void callBackMethod(char * fileName, int current, int total) {
printf("%s图片压缩的进度是:%d/%d\n", fileName, current, total);
}
void compress(char * fileName, void(*callBackP)(char *, int, int)) {
callBackP(fileName, 5, 100);
}
int main() {
void(*call) (char *, int, int); // 声明函数指针
call = &callBackMethod; // 指针指向 callBackMethod 函数
compress("123.png", call);
getchar();
return 0;
}
运行后打印如下:
123.png图片压缩的进度是:5/100
可以看到还可以把 函数指针直接声明出来,并指向另外一个函数。