接下来,C 和 C++ 的学习都将在 Visual Studio 上进行,没使用过的,可以查看教程。
第一个程序:Hello World
先来一个 Hello World:
c
#include <stdio.h> // 导入标准输入输出头文件
int main() { // 函数的主入口
printf("Hello World!");
return 0; // 程序正常退出
}
头文件(.h、.hpp)中有着函数声明和宏定义,# 开头的都是预处理指令。
例如,#include 的作用是在预处理阶段 将头文件中的声明代码复制粘贴 到指令处。然后在链接阶段 ,链接器会找到声明对应的实现(在源文件中,文件后缀为 .c、.cpp),并拼接到程序中,最终生成可执行程序。
在编译汇编阶段,会生成目标文件,检查调用到的函数是否有对应声明,没有的话会报错,例如:
c
#include <stdio.h>
int main() {
print("Hello World!"); // 标准库中不存在print的函数声明,会报错未定义标识符
return 0;
}
常用的基本数据类型
C语言中常用的基本数据类型主要有以下几种:
此外,随着标准的演进,还有
long long类型、无符号类型unsigned及布尔类型等。
c
#include <stdio.h>
int main() {
int i = 100;
double d = 200.0;
float f = 150.0f;
long l = 1000L;
short s = 20;
char c = 'c';
// 占位符打印
printf("i's value is %d\n", i); // 100
printf("d's value is %lf\n", d); // 200.000000
printf("f's value is %f\n", f); // 150.000000
printf("l's value is %ld\n", l); // 1000
printf("s's value is %d\n", s); // 20
printf("c's value is %c\n", c); // c
return 0;
}
不同的数据类型区别在于变量存储时所占用的空间不同,表示的值范围不同,并且存储的方式也有差异。
以 char 为例,它只占 1 字节,值的表示范围只有 -128 到 127。int 占 2 或 4 字节,值的范围是 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647。
int 的大小不唯一,是因为在不同位数的操作系统上有所差异,可以使用 sizeof 操作符来获取准确大小:
c
#include <stdio.h>
int main() {
int number = 100;
// sizeof 返回的是数据类型是 size_t,推荐使用 %zu 占位符
printf("number's size is %zu\n", sizeof(number)); // 获取变量的大小, 4
printf("short's size is %zu\n", sizeof(short)); // 获取类型的大小, 2
return 0;
}
如果在很老旧的编译器下不支持
%zu,可以强转为int进行打印((int)sizeof(number))。
变量的地址
每个变量在创建时,都会在内存中开辟一块空间,而该空间的位置就是变量的地址。
c
#include <stdio.h>
int main() {
int num = 200;
// %p 表示地址占位符
printf("num's address is %p", &num); // & 是取地址符
return 0;
}
运行结果可能类似:
00000063DB36FC24
可以打个断点进行调试,然后在内存窗口中查找 00000063DB36FC24 地址的值,并且将显示模式改为 4 字节整数及带符号显示,你就能够看到 200 的数据值。
获取地址处存放的值,使用的操作符是 *,也叫解引用操作符或取值符。
c
#include <stdio.h>
int main() {
int a = 5;
printf("a's value is %d\n", a);
printf("a's value is %d", *(&a));
return 0;
}
这两行打印的结果是完全相同的。
初识指针:房间与纸条
接收变量地址的变量就叫做指针(指针变量),你可以把变量看作是一个房间,房间里面有着数据,指针是记录别的房间地址的纸条,本身并没有存放具体的数据值。
例如 int a = 5;,其中 a 就是房间,进入房间就可以看见 5,我们获取值不需要额外操作。
但 int *p = &a; 就不同,p 这个纸条存放着 a 房间的详细地址,我们只有按地址找到对应房间,才能够获取 5。直接读纸条,只能得到房间的地址。
c
#include <stdio.h>
int main() {
int a = 5;
// 指针,指向了a,拿着a的地址
int* p = &a;
// 解引用:顺着纸条找到房间,读取里面的值
printf("%d", *p); // 5
// 直接打印纸条的内容:得到的是地址
printf("%p", p); // 例如 00000063DB36FC24
return 0;
}
如果你使用 %d 占位符打印指针的话,在 64 位系统上会发生数据截断,得到非真实、准确的值(指针 8 字节,%d 通常是 4 字节,高位数据被丢弃了)。
并且显示的是无意义的负数,内存地址是无符号整数,但 %d 是有符号整数的格式,经过前面的截取后,数据的最高位往往是 1,所以会显示负数,与真实的地址不符。
通过指针修改原变量
我们通过指针修改值的时候,也会修改原变量。
c
#include <stdio.h>
int main() {
int a = 5;
int* p = &a;
printf("Before, a is %d\n", a); // 5
*p = 10;
printf("After, a is %d", a); // 10
return 0;
}
为什么?
因为操作 *p 和直接操作 a 是等效的,操作的都是同一块内存空间的数据。
降维打击:为什么我们需要指针?
在 Java 中,你不能在方法内部修改外部变量本身(基本类型),这样做不到:
java
public class Demo {
public static void main(String[] args) {
int a = 10;
System.out.println(a); // 10
change(a);
System.out.println(a); // 10,外部a的值还是10
}
public static void change(int num) {
num = 999;
}
}
为什么?因为 Java 只有值传递,根本就没有暴露底层的指针。
对于基本数据类型,传的是值的副本;对象传的是引用地址的副本,在方法内部都创建了新的局部变量来接收。
而 C 语言可以通过传递地址,实现在函数内部修改外部实际参数,例如:
c
#include <stdio.h>
// 传递变量的内存地址
void change(int* p) {
// 解引用直接操作原内存
*p = 999;
}
int main() {
int a = 10;
printf("%d\n", a); // 10
change(&a);
printf("%d", a); // 999,成功修改
return 0;
}
再提一嘴,Java 对象看起来能在方法内修改外部数据,是因为它传递的是指针(引用)的副本,并且经过了安全封装,我们只能通过它来修改对象内部的属性。
java
// 实参变量
Person person = new Person();
// 形参变量
static void func(Person p){ ... }
原理:外部栈变量,存放着堆中对象的地址;内部形参也是栈上的变量,存着同一份地址。不管改动哪个,都是去修改堆上的同一块内存,不会影响到这两个栈上原始的变量本身,也就是说你不能让外部的 person 变为另一个新对象。