C语言基础快速入门与指针初探

接下来,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 变为另一个新对象。

相关推荐
Exploring3 小时前
避坑指南:升级 AGP 8.0+ 导致第三方 SDK 编译崩溃的完美解决方案
android
石山岭1 天前
自己动手写了一个 Android 虚拟定位 App:GPSSimulate 技术实
android·前端
杉氧1 天前
副作用 (Side Effects) 全攻略:如何像大师一样掌控 Composable 的生命周期?
android·架构·android jetpack
Kapaseker1 天前
Kotlin Toolchain 0.11 发布:主要是把 Amper 干没了
android·kotlin
三少爷的鞋1 天前
Android 现代架构不需要事件总线进阶篇
android
杉氧2 天前
深入理解 Compose 重组机制:快照系统如何驱动 UI 精准刷新?
android·架构·android jetpack
召钱熏2 天前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
杉氧2 天前
深度解析:Jetpack Compose 核心架构与底层原理 —— 十年安卓老兵的“破茧重生”
android·架构·android jetpack
通玄2 天前
Jetpack Compose 入门系列(七):ViewModel 与界面状态管理
android