当前位置: 首页 > news >正文

Learn C the Hardway学习笔记和拓展知识(一)

Learn C the Hardway学习笔记和拓展知识(一)

以下是《笨方法学C语言》个人学习笔记,以便日后翻阅

文章目录

  • Learn C the Hardway学习笔记和拓展知识(一)
    • 导言
    • 练习0:准备
    • 练习1:启动编辑器
      • make工具
        • make工具的基本使用方法
        • make工具的常见参数
      • 习题
    • 练习2:用Make来代替Python
      • Makefile
        • Makefile的基本结构
        • Makefile的核心语法
        • Makefile的常用模板
      • 习题
    • 练习3:格式化输出
      • printf函数
      • 习题
    • 练习4:Valgrind 介绍
      • 从源码构建可执行程序
      • 习题
    • 练习5:一个C程序的结构
      • C程序的基本结构
      • 习题
    • 练习6:变量类型
      • C语言变量类型
        • 基本变量类型
        • 派生类型
        • 空类型void
      • 习题
    • 练习7:更多变量和一些算术
      • 习题
    • 练习8:大小和数组
      • C的字符串
      • 习题
    • 练习9:数组和字符串
      • 数组和指针
        • 二者的紧密联系
        • 二者的区别
      • 习题

导言

你会学到什么?

  • C的基本语法和编写习惯。
  • 编译,make文件和链接。
  • 寻找和预防bug。
  • 防御性编程实践。
  • 使C的代码崩溃。
  • 编写基本的Unix系统软件。

练习0:准备

安装依赖:

sudo apt-get install build-essential

编辑器:使用Vim,或者使用Nano(小白友好)

关于Vim的使用可以参看博主的Linux命令行系列笔记

练习1:启动编辑器

make工具

这一部分需要我们熟悉一下make工具。

make是一个在软件开发中被广泛使用的自动化构建工具。它通过读取一个名为Makefile的脚本文件,来高效、智能地管理和执行项目的编译、链接等任务。

make是工具,而Makefile是文件

关于Makefile我们之后再说,这是make之所以高效的精髓所在。这里我们先聚焦于make的使用。

make工具的基本使用方法
  1. 执行默认任务
    这是最基本、最常用的命令。在项目根目录下,不带任何参数执行make。
$ make

作用:此命令会执行Makefile中定义的默认目标。按照惯例,默认目标通常是编译整个项目,生成最终的可执行文件或库。

注意:make会自动检查源文件和目标文件的修改时间。如果它发现源文件(如 .c 文件)比目标文件(如 .o 或可执行文件)要新,它才会执行编译命令。如果所有文件都是最新的,它会提示“目标已是最新”,什么也不做,从而极大地节省了时间。

如果没有Makefile,make工具会尝试使用内置的隐式规则构建可执行文件,但如果隐式规则执行失败就会报错。

  1. 执行指定任务
    您可以明确告诉make您想执行哪一个具体任务。

# 执行名为“clean”的任务
$ make clean# 执行名为“install”的任务
$ make install# 执行名为“test”的任务
$ make test

这些是Makefile中常见的预定义任务,需要你在Makefile里进行编写

作用:此命令会执行Makefile中与指定目标(clean, install, test)相对应的规则。

  • clean:清理项目目录,删除所有编译过程中产生的中间文件和最终产物。
  • install:将编译好的程序或库安装到系统中指定的位置(如 /usr/local/bin)。
  • test:运行项目的单元测试或集成测试。

提示:您可以通过Tab键的自动补全功能来查看一个项目支持哪些任务目标(需要shell环境配置支持)。输入 make 然后按两下 Tab 键,通常会列出所有可用的目标。

  1. 执行多个任务
    您可以让make按顺序连续执行多个任务。
$ make clean all

作用:make会按照您给出的顺序,依次执行每个任务。在上述例子中,它会先执行clean任务来删除旧文件,然后执行all任务(all通常是默认任务的别名),从一个干净的状态重新编译整个项目。这在您想要彻底重新构建时非常有用。

make工具的常见参数
  • -j 或 --jobs= (并行构建)
    这是提升编译效率最重要的参数。它让make可以同时执行N个命令。
# 使用 8 个线程并行编译
$ make -j 8# 让 make 自行决定并行任务数(通常是CPU核心数)
$ make -j

适用场景:在多核CPU的机器上编译大型项目,可以成倍缩短等待时间。

  • -n 或 --just-print (空运行/演习模式)
    只打印出将要执行的命令,但并不真正执行它们。
$ make -n install

作用:在执行一个有潜在风险或不确定的任务(如install)之前,用此参数可以预先查看make到底打算执行哪些shell命令,以确保一切符合预期。是调试Makefile和安全操作的利器。

  • -k 或 --keep-going (持续执行)

在构建过程中,如果某个命令出错,make默认会立即停止。使用此参数后,如果某个分支的构建失败,make会继续尝试构建其他不依赖于失败分支的目标。

  • -C <dir> 或 --directory=<dir> (指定目录)

在执行任何操作前,先切换到 <dir> 目录。

$ make -C /path/to/project/subdir

作用:让您可以从任何位置去调用特定子目录中的make任务,而无需先cd过去。在编写更复杂的构建脚本时非常有用。

  • -B 或 --always-make (强制重新构建)

无条件地认为所有目标都已过期,强制重新构建所有东西。

$ make -B

作用:当您怀疑文件的时间戳可能不正确,导致make没有重新编译应更新的文件时,可以使用此参数强制进行一次完整的构建。

  • -f <file>或 --file=<file> (指定Makefile文件)

使用一个非标准名称的Makefile文件。make默认会寻找GNUmakefile, makefile, Makefile

$ make -f MyMakefile.mk

作用:当项目中有多个Makefile,用于不同的构建场景(如生产环境、测试环境)时,可以用此参数来选择。

习题

都比较简单,值得说明一下的是man 3 puts里的3的含义:这是手册的章节编号。man的手册被分成了不同的章节,以便对内容进行分类。章节编号3里写的是库函数。

这一章专门收录C语言标准库(libc)或者其他库(如数学库libm)提供的函数。例如 printf, malloc, strcpy, 以及我们这里的 puts 都属于这一章。

练习2:用Make来代替Python

Makefile

Makefile的基本结构

Makefile的基本语法:

<目标 (Target)>: <依赖 (Prerequisites)><Tab><命令 (Command)>
  • 目标 (Target)

    • 通常是一个文件名,是你希望生成的东西,比如可执行文件my_app或目标文件main.o。
    • 也可以是一个“动作”的名称,比如clean,这种不代表实际文件的目标被称为“伪目标”(Phony Target)。
  • 依赖 (Prerequisites)

    • 生成该“目标”所需要的一个或多个文件(或其他目标)。
    • make会检查“目标”文件和“依赖”文件的时间戳。如果任何一个依赖比目标更新(或者目标文件不存在),make就会执行下面的命令来重新生成目标。
  • 命令 (Command)
    用于从“依赖”生成“目标”的Shell命令。

极其重要!!! 每条命令都必须以一个 制表符(Tab) 开头,绝不能是空格!这是初学者最常犯的错误,会导致make报missing separator. Stop.的错误。

一个示例:

# 目标是hello, 依赖是hello.c
hello: hello.c# 命令是使用gcc编译hello.c生成hellogcc hello.c -o hello
Makefile的核心语法
  1. 变量

定义变量:

  • =(延时展开):变量的值在使用时才被确定。
  • :=(立即展开):变量的值在定义时就被立即确定。推荐优先使用:=,因为它的行为更直观,可以避免由延时展开引起的潜在问题(如无穷递归)。
  • ?=(条件赋值):如果变量尚未定义,则为它赋值。如果已定义,则什么也不做。
  • +=(追加赋值):为变量追加内容。

使用变量:通过 $(VAR_NAME) 或 ${VAR_NAME} 来引用。

例子:

# 定义变量 (推荐使用 :=)
CC := gcc
CFLAGS := -Wall -g  # -Wall: 显示所有警告, -g: 添加调试信息
TARGET := my_app
SRCS := main.c utils.c
OBJS := $(SRCS:.c=.o) # 这是一个高级用法,将.c后缀替换为.o# 第一个目标通常是 'all',它依赖于最终的可执行文件
all: $(TARGET)# 链接规则:从所有.o文件生成最终目标
$(TARGET): $(OBJS)$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)# 编译规则:从.c文件生成.o文件
# 这里可以为每个文件写一条规则,但非常繁琐
main.o: main.c$(CC) $(CFLAGS) -c main.cutils.o: utils.c$(CC) $(CFLAGS) -c utils.c

自动化变量
在规则的命令中,可以使用自动化变量来指代目标和依赖,极大简化写法。

  • $@:表示规则中的目标文件名。
  • $^:表示规则中的所有依赖文件列表(以空格分隔,无重复)。
  • $<:表示规则中的第一个依赖文件名。
  • $?:表示所有比目标新的依赖文件列表。
  1. 规则模式
    为了避免给每一个.c文件都写一条编译规则,我们可以使用模式规则,用%作为通配符。
# 模式规则:定义了如何从任一.c文件生成对应的.o文件
%.o: %.c# $@ 代表目标 (如 main.o)# $< 代表第一个依赖 (如 main.c)$(CC) $(CFLAGS) -c $< -o $@

有了这条模式规则,上面例子中针对main.o和utils.o的单独规则就可以全部删除了,make会自动应用它。

  1. 伪目标(.PHONY)

伪目标不代表真实文件,它只是一个执行命令的标签。使用.PHONY声明一个目标,可以:

  • 避免当目录下恰好有一个同名文件时,导致命令无法执行的问题。
  • 提高性能,因为make不会去检查该文件是否存在或其时间戳。

常见的几个伪目标:
all:

  • 作用:作为Makefile的默认入口,用于编译整个项目的主要部分。它通常是Makefile中的第一个目标,这样当用户只输入make而不带任何参数时,就会默认执行all。
  • 写法:all的依赖通常是项目最终要生成的那些可执行文件或库文件。
.PHONY: all
all: my_app my_library.so
my_app: main.o utils.o$(CC) -o $@ $^
my_library.so: feature.o$(CC) -shared -o $@ $^
# ... 其他编译规则 ...

clean:

  • 作用:清理构建产物。删除所有由make命令创建的中间文件(如.o文件)和最终产品(可执行文件、库文件等),但保留源代码和配置文件。
  • 写法:命令通常是rm -f …。使用-f参数可以确保在文件不存在时命令不会报错。
.PHONY: clean
clean:rm -f *.o my_app my_library.so

install:

  • 作用:安装程序。将编译好的可执行文件、库文件、文档等拷贝到系统的标准位置(如/usr/local/bin, /usr/local/lib等)。
  • 写法:命令通常是cp或install命令。由于安装到系统目录通常需要管理员权限,所以命令前有时会加上sudo,或者在文档中提示用户使用sudo make install。
.PHONY: install
install: allinstall -m 755 my_app /usr/local/bin/install -m 644 my_library.so /usr/local/lib/

注意:install目标通常依赖于all,以确保在安装前所有东西都已被正确编译。

uninstall:

  • 作用:卸载程序。install的逆操作,从系统中删除由make install安装的文件。提供这个目标是一个非常好的习惯。
.PHONY: uninstall
uninstall:rm -f /usr/local/bin/my_apprm -f /usr/local/lib/my_library.so

test或check:

  • 作用:运行测试。执行项目的单元测试、集成测试或任何自动化测试脚本。
.PHONY: test
test: all./run_tests.sh# 或者直接调用测试程序./my_app --test
  1. 内置函数

Makefile提供了一些内置函数,用于处理文件名和字符串。

  • $(wildcard PATTERN):查找匹配PATTERN的所有文件。
    示例:SRCS := $(wildcard *.c) 会自动获取当前目录下所有的.c文件。
  • $(patsubst PATTERN, REPLACEMENT, TEXT):模式字符串替换。
    示例:OBJS := $(patsubst %.c, %.o, $(SRCS)) 会将SRCS变量中所有以.c结尾的字符串替换为以.o结尾。
  1. 隐藏命令(@)和忽略错误(-)
  • 在命令前加上@,make在执行时将不会打印该命令本身,只会显示其输出。这可以让构建日志更干净,如@echo “Linking…”。
  • 在命令前加上-,make会忽略该命令的执行错误,继续执行后续任务。最常见的用法是clean目标中的-rm,这样即使没有文件可供删除,make也不会报错。
  1. 条件判断

Makefile里的条件判断主要用于:

  • 适应不同操作系统:根据当前是Linux、macOS还是Windows,执行不同的命令或使用不同的编译选项。
  • 区分不同构建类型:轻松实现“调试版(Debug)”和“发行版(Release)”的切换。调试版可能需要加入调试符号,而发行版需要加入优化选项。
  • 启用或禁用可选功能:根据用户的选择,决定是否编译某个可选模块或链接某个可选库。
  • 提供默认配置:如果用户没有指定某个变量(如编译器),则为其设置一个合理的默认值

两种基本判断结构

  • ifeq和ifneq
    用于判断两个参数是否相等。这是最常用的判断指令。

ifeq (arg1, arg2)# 如果 arg1 和 arg2 相等,则执行这里的语句
else # 否则
endif

ifneq用于判断两个参数是否不相等,与ifeq逻辑相反。

  • ifdef和ifndef
    用于判断一个变量是否已经被定义。只要变量有值(即使是空值,通过VAR=定义),ifdef就判断为真。
ifdef variable_name# 如果 variable_name 已被定义,则执行这里的语句
endif

用于判断一个变量是否尚未被定义,与ifdef逻辑相反。

注意:

  • else: 提供“否则”的分支。一个if语句块中最多只能有一个else。
  • endif: 必须有! 每一个if…语句块都必须以一个endif结尾,标志着条件判断的结束。

一个示例,用于处理跨平台兼容:

# 使用make内置变量OS,它通常在Windows上为"Windows_NT"
ifeq ($(OS), Windows_NT)RM := del /QEXE_EXT := .exe# 在Windows上可能不需要链接特殊库LIBS :=
elseRM := rm -fEXE_EXT :=# 在Linux/macOS上需要链接pthread库LIBS := -lpthread
endifTARGET := my_app$(EXE_EXT)all: $(TARGET)$(TARGET): main.o$(CC) -o $@ $^ $(LIBS)# ...
.PHONY: clean
clean:$(RM) $(TARGET) *.o
Makefile的常用模板
# =================================================================================== #
#                             Makefile 通用模板                                     #
# =================================================================================== ## =========================      1. 项目基础配置      ============================== #
# 用户主要修改此区域# 1.1) 定义最终生成的可执行文件名
TARGET      := my_app# 1.2) 定义源文件目录和文件扩展名
# (自动发现所有.c或.cpp文件)
SRC_DIR     := src
SRC_EXT     := .c
SRCS        := $(wildcard $(SRC_DIR)/*$(SRC_EXT))# 1.3) 定义目标文件(.o)的输出目录
# (根据源文件自动生成对应的.o文件名列表)
OBJ_DIR     := obj
OBJS        := $(patsubst $(SRC_DIR)/%$(SRC_EXT), $(OBJ_DIR)/%.o, $(SRCS))# 1.4) 定义头文件目录
INC_DIR     := include
INC_FLAGS   := -I$(INC_DIR)# =========================      2. 编译器与编译选项      =========================== ## 2.1) 定义编译器
CC          := gcc
CXX         := g++# 2.2) 定义编译和链接选项
# CFLAGS: C文件编译选项
# CXXFLAGS: C++文件编译选项
# LDFLAGS: 链接器选项 (如-L/path/to/libs)
# LIBS: 需要链接的库 (如-lm, -lpthread)
# DEFINES: 预定义的宏 (如-DDEBUG)
CFLAGS      := -Wall -g
CXXFLAGS    := $(CFLAGS) -std=c++11
LDFLAGS     :=
LIBS        :=
DEFINES     :=# =========================    3. 构建模式 (调试 vs 发行)   ========================= #
# 通过 `make DEBUG=1` 来启用调试模式# 默认为发行模式 (Release Mode)
BUILD_MODE  := Release
CFLAGS      += -O2
CXXFLAGS    += -O2# 如果检测到 "DEBUG=1", 则切换到调试模式 (Debug Mode)
ifeq ($(DEBUG), 1)BUILD_MODE  := Debug# 调试模式下,使用-g选项并定义DEBUG宏CFLAGS      = -Wall -Wextra -gCXXFLAGS    = $(CFLAGS) -std=c++11DEFINES     += -DDEBUG# 可以给调试版的目标文件加一个后缀TARGET      := $(TARGET)_debug
endif# =========================       4. 核心构建规则        =========================== ## 伪目标声明
.PHONY: all clean rebuild install uninstall help# ---- 默认目标: all ----
# 第一个目标是默认目标,当只输入`make`时执行
all: $(TARGET)@echo "=========================================================="@echo " Project: $(TARGET)"@echo " Build Mode: $(BUILD_MODE)"@echo " Build completed successfully!"@echo "=========================================================="# ---- 链接规则 ----
# 将所有的.o文件链接成最终的可执行文件
$(TARGET): $(OBJS)@echo "-> Linking target: $@"$(CC) $^ $(LDFLAGS) $(LIBS) -o $@# ---- 编译规则 (模式规则) ----
# 定义如何从.c文件编译出.o文件
# 命令会为每个.o文件执行一次
$(OBJ_DIR)/%.o: $(SRC_DIR)/%$(SRC_EXT)@echo "-> Compiling source: $<"# 在编译前确保输出目录存在@mkdir -p $(OBJ_DIR)$(CC) $(CFLAGS) $(DEFINES) $(INC_FLAGS) -c $< -o $@# =========================      5. 其他常用伪目标      =========================== ## ---- 清理目标 ----
clean:@echo "-> Cleaning project..."-rm -rf $(OBJ_DIR) $(TARGET) $(TARGET)_debug@echo "-> Clean complete."# ---- 强制重新构建 ----
rebuild: clean all# ---- 安装目标 ----
install: all@echo "-> Installing $(TARGET)..."# 这里需要根据你的需求修改安装路径和权限# sudo install -m 755 $(TARGET) /usr/local/bin/# ---- 卸载目标 ----
uninstall:@echo "-> Uninstalling $(TARGET)..."# sudo rm -f /usr/local/bin/$(TARGET)# ---- 帮助目标 ----
help:@echo "Usage: make [TARGET]"@echo "----------------------------------------------------------"@echo "Available targets:"@echo "  all       - Build the project (default)."@echo "  clean     - Remove all build artifacts."@echo "  rebuild   - Clean and then build the project."@echo "  install   - Install the application (requires permission)."@echo "  uninstall - Uninstall the application (requires permission)."@echo ""@echo "Options:"@echo "  DEBUG=1   - Build in debug mode."@echo "            Example: make DEBUG=1"@echo "----------------------------------------------------------"

模板使用的目录结构如下,当然也可以直接在模板中的参数中进行修改。

my_project/
├── Makefile
├── src/
│ ├── main.c
│ └── utils.c
└── include/
└── utils.h

修改配置:

  • 将 TARGET := my_app 修改为你想要的可执行文件名。
  • 如果你的源文件扩展名不是 .c,修改 SRC_EXT。
  • 如果需要链接其他库,在 LIBS 后面添加,例如 LIBS := -lm -lpthread。

执行命令:

  • make: 编译生成发行版 my_app。
  • make DEBUG=1: 编译生成调试版 my_app_debug。
  • make clean: 清理所有生成的文件。
  • make rebuild: 强制重新编译所有文件。
  • make help: 查看所有可用的命令。

习题

定义all后直接使用make进行项目构建:

CC=gcc
CFLAGS=-Wall -gall:ex1ex1:ex1.o${CC} $^ ${CFLAGS} -o $@ex1.o:ex1.c${CC} ${CFLAGS} -c $< -o $@clean:-rm -f ex1 ex1.o

练习3:格式化输出

printf函数

C语言中的格式化输出主要通过printf函数来实现,printf 函数是 “print formatted” 的缩写,它定义在标准输入输出头文件 <stdio.h> 中。

基本语法如下:

printf("格式控制字符串", 参数1, 参数2, ...);
  • 格式控制字符串 (Format Control String): 这是一个字符串,包含了两种内容:
    • 普通字符: 这些字符会原样输出到屏幕上。
    • 格式占位符 (Format Specifier): 这些字符以 % 开头,用于为后续的参数指定一个“位置”和输出的“格式”。
  • 参数列表 (Argument List): 这是要输出的变量、常量或表达式的列表。参数的数量必须与格式控制字符串中占位符的数量相匹配,并且类型也要对应。

常见占位符

占位符对应数据类型描述示例代码
%d 或 %iint用于输出一个有符号的十进制整数。int age = 30; printf(“年龄: %d\n”, age);
%ffloat, double用于输出一个十进制浮点数(小数)。默认显示6位小数。 float pi = 3.14159; printf(“Pi: %f\n”, pi);
%e 或 %Efloat,double以科学技术法输出一个数字double num = 12345.6789; printf(“%e\n”, num); // 输出:1.234568e+04
%cchar用于输出一个单一字符。char grade = ‘A’; printf(“等级: %c\n”, grade);
%schar * (字符串)用于输出一个字符串。参数应为一个指向字符数组的指针。char name[] = “Alice”; printf(“你好, %s!\n”, name);
%uunsigned int用于输出一个无符号的十进制整数。unsigned int count = 100; printf(“数量: %u\n”, count);
%pvoid * (指针)以十六进制的形式输出一个指针变量所存储的内存地址。int num = 10; printf(“num的地址是: %p\n”, &num);
%x 或 %Xint以小写(%x)或大写(%X)的十六进制形式输出整数。int hex_val = 255; printf(“十六进制: %x\n”, hex_val);
%oint以八进制形式输出整数。int oct_val = 64; printf(“八进制: %o\n”, oct_val);
%%(无)用于输出一个百分号 % 本身。printf(“折扣: 20%%\n”);

修饰符
除了基本的类型说明,占位符还可以包含修饰符,用来更精确地控制输出的格式,例如对齐方式、宽度、精度等。修饰符放在 % 和类型字符之间,其一般形式为:

%[标志][宽度][.精度][长度]类型
  1. 宽度

指定输出所占的最小字符数。如果实际输出的字符数小于指定宽度,printf 会用空格进行填充(默认是右对齐)。

  • %5d: 输出一个整数,至少占5个字符的宽度。如果整数是 123,输出会是 123(前面有两个空格)。
  • %10s: 输出一个字符串,至少占10个字符的宽度。
  1. 精度
    用一个点 . 后跟一个数字来表示。它的含义取决于占位符的类型:
  • 对于整数 (d, u, x, o): 指定要输出的最少数字位数。如果实际位数不够,会在前面补0。
  • 对于浮点数 (f): 指定小数点后要显示的位数。
  • 对于字符串 (s): 指定要输出的最大字符数
  1. 标志
    标志是一些特殊字符,可以改变输出的行为。
  • - (减号): 左对齐。默认是右对齐。
  • + (加号): 强制显示正负号。对于正数,会显示 + 号;对于负数,显示 - 号。
  • (空格): 如果是正数,在前面留一个空格;如果是负数,则显示负号。
  • 0 (零): 使用前导零来填充宽度,而不是空格(仅当没有使用 - 标志时有效)。
  1. 长度

长度修饰符用于指定参数的实际数据类型比默认类型更长或更短。

  • h: 用于 short int (%hd) 或 unsigned short int (%hu)。
  • l (小写L): 用于 long int (%ld) 或 unsigned long int (%lu),以及 double (%lf,虽然在 printf 中 double 和 float 都用 %f,但在 scanf 中必须用 %lf 区分)。
  • ll (两个小写L): 用于 long long int (%lld) 或 unsigned long long int (%llu)。
  • L: 用于 long double (%Lf)。

一个综合示例:

#include <stdio.h>int main() {char item[] = "Laptop";int id = 78;float price = 1250.75f;int stock = 5;printf("=========================================\n");printf("| %-15s | %-6s | %-10s |\n", "商品", "库存", "价格");printf("-----------------------------------------\n");printf("| %-15s | %06d | $%10.2f |\n", item, stock, price);printf("=========================================\n");return 0;
}

输出如下:

=========================================
| 商品            | 库存   | 价格       |
-----------------------------------------
| Laptop          | 000005 | $   1250.75 |
=========================================

习题

Makefile文件如下

CC=gcc
CFLAGS=-Wall -g
TARGET=ex3.PHONY: all cleanall:${TARGET}${TARGET}:${TARGET}.c${CC} ${CFLAGS} $^ -o $@clean:-rm -f ${TARGET} *.o

练习4:Valgrind 介绍

从源码构建可执行程序

本节介绍了一种非常好用的从源码构建可执行文件的步骤:

  • 下载源码的归档文件来获得源码
  • 解压归档文件,将文件提取到你的电脑上
  • 运行./configure来建立构建所需的配置
  • 运行make来构建源码,就像之前所做的那样
  • 运行sudo make install来将它安装到你的电脑

值得说明的是我使用如下命令获取的源码(教程中给的curl方式我试了不行):

wget https://sourceware.org/pub/valgrind/valgrind-3.25.1.tar.bz2

习题

简单看了一下源码中的configure脚本和Makefile脚本,都是成千上万行的。

练习5:一个C程序的结构

C程序的基本结构

这部分比较简单,就不做笔记了

习题

练习6:变量类型

C语言变量类型

这一节我们主要说明一下C语言中的变量类型主要有哪些。

基本变量类型
  1. 整形:int char long short

用于存储整数。根据存储空间大小和是否有符号,整型可以进一步细分。

类型存储大小 (典型)值范围格式化说明符
char1 字节-128 到 127 或 0 到 255%c
unsigned char1 字节0 到 255%c
signed char1 字节-128 到 127%c
short 或 short int2 字节-32,768 到 32,767%hd
unsigned short2 字节0 到 65,535%hu
int4 字节 (常见)-2,147,483,648 到 2,147,483,647%d, %i
unsigned int4 字节 (常见)0 到 4,294,967,295%u
long 或 long int4 或 8 字节取决于系统%ld
unsigned long4 或 8 字节取决于系统%lu
long long8 字节-(2^63) 到 (2^63)-1%lld
unsigned long long8 字节0 到 (2^64)-1%llu

注意:

  • char 类型在技术上是整型,因为它可以存储-128到127之间的整数,但它通常用来表示ASCII字符。
  • signed 和 unsigned 是类型修饰符。signed 表示该变量可以存储正数、负数和零,而 unsigned 表示只能存储非负数。默认情况下,整型(char 除外)都是 signed 的。
  • int 的大小依赖于编译器和操作系统,但通常是4字节。
  1. 浮点型:float double
    用于存储带有小数部分的数值。
类型存储大小 (典型)精度格式化说明符
float4 字节大约 6-7 位十进制有效数字%f
double8 字节大约 15-16 位十进制有效数字%lf
long double通常大于 double更高精度%Lf

要点:

  • double 提供了比 float更高的精度,因此在科学计算和需要高精度的场合更为常用。
  • 在进行浮点数比较时,由于精度问题,直接使用 == 进行比较可能会得到意想不到的结果,通常建议检查两个数的差值是否在一个很小的范围内
  1. 枚举型:enum
    enum 关键字用于定义一个枚举类型,它是一组命名的整型常量。这使得代码更具可读性和可维护性。

示例:

enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
enum Weekday today = WEDNESDAY;

默认情况下,枚举列表中的第一个成员值为0,后续成员依次加1。也可以手动为枚举成员指定整数值。

派生类型
  1. 数组
    数组是相同数据类型元素的集合,这些元素在内存中是连续存储的。
  • 声明: type arrayName[arraySize];
    示例:
int numbers[10]; // 声明一个包含10个整数的数组
numbers[0] = 5;  // 给第一个元素赋值
  1. 指针
    指针是一种特殊的变量,它存储的是另一个变量的内存地址。
  • 声明: type *pointerName;
    示例:
int var = 20;
int *ip;      // 声明一个整型指针
ip = &var;    // 将var的地址赋给指针ip

指针是C语言强大功能的核心,广泛用于动态内存分配、函数参数传递和数据结构实现。

  1. 结构体struct

结构体 (struct) 允许将不同数据类型的变量组合成一个单一的单元。这对于表示真实世界的对象非常有用。

定义与使用:


struct Person {char name[50];int age;float salary;
};struct Person p1;
strcpy(p1.name, "John Doe");
p1.age = 30;
  1. 联合体union

联合体 (union) 也允许存储不同数据类型的变量,但与结构体不同的是,它的所有成员共享同一块内存空间。这意味着在任何时候,联合体只能存储其中一个成员的值。

定义与使用:

union Data {int i;float f;char str[20];
};union Data data;
data.i = 10;
// 此时 data.f 和 data.str 的值是未定义的

联合体主要用于节省内存,当需要在不同时间存储不同类型的数据时非常有用。

空类型void

void 类型是一个特殊的类型,它表示“没有类型”或“无值”。它的主要用途有:

  1. 函数返回类型: 当函数不返回任何值时,其返回类型应声明为 void。

void printMessage() {printf("Hello, World!\n");
}
  1. 函数参数: 当函数不接受任何参数时,可以在括号内使用 void。
int getRandomNumber(void);
  1. 通用指针: void * 是一种通用指针,可以指向任何数据类型的地址。但在使用它之前,必须将其强制转换为具体的指针类型。
int x = 10;
void *ptr = &x;
int *int_ptr = (int *)ptr;

习题

#include<stdio.h>int main(int argc, char* argv[]){char none[] = "";printf("Here is nothing:%s.\n",none);int n = 16;printf("Hex:%x,Dec:%d,Oct:%o\n",n,n,n);return 0;
}

练习7:更多变量和一些算术

这部分笔记参看第3和第6个练习的笔记。

习题

将数字改得太大会造成溢出,对于符号数来说,一个太大的正数很可能会造成输出为一个负数(溢出)。

练习8:大小和数组

C的字符串

跟着这一题,可以简单聊一聊C的字符串。我们知道C语言中char类型就是int类型,那么C语言中的字符串呢?其实,C的字符串就是char的数组。说白了,C语言中并没有专门的字符串类型,当我们初始化一个字符串时,会发生如下的事情。例如,当我们写 char str[] = “hello”; 时,C语言在内存中创建了这样一块区域:

内存地址0x10000x10010x10020x10030x10040x1005
内容‘h’‘e’‘l’‘l’‘o’\0

你会发现,虽然 “hello” 只有5个字符,但它实际占用了6个字节的内存空间。最后一个字节存储的就是 \0。\0 是一个值为0的特殊字符,它的唯一使命就是告诉程序:“字符串到此结束。”

C语言中所有处理字符串的标准库函数(定义在 <string.h> 和 <stdio.h> 等头文件中)都依赖于 \0 来确定字符串的边界。这些函数不会预先知道字符串有多长,它们从字符串的起始地址开始,逐个字节地向后扫描,直到遇到第一个 \0 为止。

习题

一些简单的赋值操作

#include <stdio.h>int main(int argc, char *argv[])
{int areas[] = {10, 12, 13, 14, 20};char name[] = "Zed";char full_name[] = {'Z', 'e', 'd',' ', 'A', '.', ' ','S', 'h', 'a', 'w','\0'};// WARNING: On some systems you may have to change the// %ld in this code to a %u since it will use unsigned intsprintf("The size of an int: %ld\n", sizeof(int));printf("The size of areas (int[]): %ld\n",sizeof(areas));printf("The number of ints in areas: %ld\n",sizeof(areas) / sizeof(int));areas[0] = 1;areas[1] = name[1];printf("The first area is %d, the 2nd %d.\n",areas[0], areas[1]);printf("The size of a char: %ld\n", sizeof(char));printf("The size of name (char[]): %ld\n",sizeof(name));name[1] = 'a';printf("The number of chars: %ld\n",sizeof(name) / sizeof(char));printf("The size of full_name (char[]): %ld\n",sizeof(full_name));printf("The number of chars: %ld\n",sizeof(full_name) / sizeof(char));printf("name=\"%s\" and full_name=\"%s\"\n",name, full_name);return 0;
}

练习9:数组和字符串

数组和指针

这一题引出了C语言中数组个指针的关系问题。首先需要明确,C语言中的数组和指针是不同的东西,但是二者却紧密练习,在很多时候可以互换使用。

二者的紧密联系
  1. 数组名代表首元素地址
    数组名本身就包含了数组的起始地址信息。
int a[10];
printf("%p\n", a);      // 输出数组 a 的首元素地址
printf("%p\n", &a[0]);  // 同样输出数组 a 的首元素地址

在这段代码中,a 和 &a[0] 的值是完全相同的。a 表现得就像一个指向 a[0] 的指针。

  1. 数组下标 [] 与指针运算 *() 的等价性

C语言的语法设计使得访问数组元素时,下标运算和指针运算在功能上是等价的。

  • a[i] 在C语言编译器内部,实际上就是被解释为 *(a + i)。
  • a:数组首元素的地址。
  • a + i:一个指针运算。根据指针的类型(这里是 int *),地址会向前移动 i * sizeof(int) 个字节,从而指向第 i 个元素(从0开始计数)的地址。

*(a + i):对计算出的地址进行解引用(dereference),获取该地址上存储的值。

因此,以下两种写法是完全等价的:


int a[10] = {0, 10, 20, 30, 40};
int i = 2;// 两种等价的访问方式
printf("Value: %d\n", a[i]);       // 输出 20
printf("Value: %d\n", *(a + i));   // 同样输出 20// 两种等价的获取地址的方式
printf("Address: %p\n", &a[i]);     // 输出第2个元素的地址
printf("Address: %p\n", a + i);     // 同样输出第2个元素的地址

由于加法满足交换律,*(a + i) 和 *(i + a) 是一样的,所以甚至可以写出 i[a] 这种奇怪但合法的代码,它和 a[i] 的效果完全一样。

  1. 将数组传递给函数

当你将一个数组作为参数传递给函数时,实际上传递的并不是整个数组的拷贝,而仅仅是该数组首元素的地址。因此,在函数内部,接收这个数组的参数实际上是一个指针。

void myFunction(int arr[]) {// 尽管形参写成数组形式,但编译器会把它当作指针处理// sizeof(arr) 在这里得到的是指针的大小(如4或8),而不是整个数组的大小printf("Inside function: sizeof(arr) = %zu\n", sizeof(arr));
}int main() {int a[10];printf("In main: sizeof(a) = %zu\n", sizeof(a)); // 输出 10 * sizeof(int) = 40myFunction(a);                                  // 输出 4 或 8return 0;
}

在 myFunction 的声明中,int arr[] 和 int *arr 是完全等价的。这也就是为什么在函数内部无法通过 sizeof 获取数组原始大小的原因,因为它接收到的只是一个指针。

二者的区别
  1. 内存分配方式不同
  • 数组:在定义时,编译器会为其分配一块连续的内存空间,用于存储数组的所有元素。数组名 a 可以看作是这块内存的“别名”或“标签”,它与这块内存紧密关联。
  • 指针:是一个变量,它只占用足以存储一个内存地址的空间(在32位系统上是4字节,64位系统上是8字节)。这个变量的值是另一个变量的地址。
int a[10];      // 分配了 10 * sizeof(int) 的连续空间
int *p;         // 只分配了 sizeof(int*) 的空间来存放一个地址
p = a;          // 指针 p 的值现在是数组 a 的首地址
  1. sizeof 运算符的结果不同

这是区分数组和指针最直接的方法。

  • sizeof(数组名):返回的是整个数组占用的总字节数。
  • sizeof(指针):返回的是指针变量自身占用的字节数(4或8)。
int a[10];
int *p = a;
printf("sizeof(a) = %zu\n", sizeof(a)); // 输出 40 (假设 int 是 4 字节)
printf("sizeof(p) = %zu\n", sizeof(p)); // 输出 8 (假设是 64 位系统)
  1. 数组名是常量,指针是变量
  • 数组名:它是一个常量左值 (non-modifiable lvalue)。它代表了数组首元素的地址这个常量,你不能修改数组名本身的值,即不能让它指向其他地方。
  • 指针:它是一个变量。你可以随意修改指针的值,让它指向不同的内存地址。
int a[10];
int b[10];
int *p;p = a;          // 正确:p 指向 a 的开头
p = b;          // 正确:p 现在指向 b 的开头
p++;            // 正确:p 指向 a[1]a = b;          // 错误!不能修改数组名 'a' 的值
a++;            // 错误!不能修改数组名 'a' 的值

这个区别是根本性的。数组名是其内存块的“身份标识”,而指针是存储“地址信息”的容器。你不能改变一个内存块的“身份”,但可以改变容器里的内容。

简单来说,它们的关系可以概括为一句话:在大多数表达式中,数组名会被隐式地“降维”为一个指向其首元素的常量指针。

习题

#include <stdio.h>int main(int argc, char *argv[])
{int numbers[4] = {0};char name[4] = {'a'};// first, print them out rawprintf("numbers: %d %d %d %d\n",numbers[0], numbers[1],numbers[2], numbers[3]);printf("name each: %c %c %c %c\n",name[0], name[1],name[2], name[3]);printf("name: %s\n", name);// setup the numbersnumbers[0] = 'a';numbers[1] = 2;numbers[2] = 3;numbers[3] = 4;// setup the namename[0] = 123;name[1] = 'e';name[2] = 'd';name[3] = '\0';// then print them out initializedprintf("numbers: %d %d %d %d\n",numbers[0], numbers[1],numbers[2], numbers[3]);printf("name each: %c %c %c %c\n",name[0], name[1],name[2], name[3]);// print the name like a stringprintf("name: %s\n", name);// another way to use namechar *another = "Zed";printf("another: %s\n", another);printf("another each: %c %c %c %c\n",another[0], another[1],another[2], another[3]);printf("name each: %c %c %c %c\n",*(name),*(name+1),*(name+2),*(name+3));return 0;
}

关于题目“如果一个字符数组占四个字节,一个整数也占4个字节,你可以像整数一样使用整个name吗?你如何用黑魔法实现它?”,我的思考就是使用指针操作。即定义一个4字节整数的指针,并将指针指向name,最后利用指针给整数赋值即可。

http://www.dtcms.com/a/487447.html

相关文章:

  • 算法10.0
  • 凡科网做的网站能直接用吗网站换服务器对排名有影响吗
  • 多层超表面革新 | 简化传统光学系统
  • 辽阳专业建设网站公司电话山东住房城乡建设厅网站首页
  • 数据结构2:线性表1-线性表类型及其特点
  • 网站外包如何报价做那种事的网站
  • 张家港做网站的推荐驻马店app和网站开发公司
  • 目标检测(一)
  • 石家庄免费做网站专做药材的网站有哪些
  • 基本功 | 一文讲清多线程和多线程同步
  • 360门户网站怎样做广州百度seo代理
  • C++蓝桥杯之函数与递归
  • Oracle AWR报告分析:诊断RAC Global cache log flush性能故障
  • python - 第四天
  • 领取流量网站药剂学教学网站的建设
  • 端端网站开发网络广告网站怎么做
  • threejs(五)纹理贴图、顶点UV坐标
  • debug - MDK - arm-none-eabi - 将MDK工程编译过程的所有命令行参数找出来
  • 网站怎么维护百度会收录双域名的网站么
  • Oracle数据库基本命令的8个模块
  • Vue3中的计算属性和监视属性【5】
  • Docker部署WordPress及相关配置
  • 大自然的网站设计营销型企业网站源码
  • 网站如何做线上支付功能免费刷推广链接的网站
  • 使用Flask部署PyTorch模型
  • 新版视频直播点播平台EasyDSS用视频能力破局!
  • python_视频切分
  • vscode 侧边文件夹名字体大一点
  • C++ 进阶特性深度解析:从友元、内部类到编译器优化与常性应用
  • Linux 线程与页表