自己整理的C语言零基础学习笔记,只有一小部分,大概率还会更新。
例1:helloworld
//导入头文件,std表示标准库,io表示输入输出,所以这是一个“标准输入输出库”
//include不限于头文件,可导入任何文件
//include后面接<>时表示导入系统文件,接""表示导入自定义文件
#include <stdio.h>
//程序有且只有一个主函数(main函数)
int main()
{
//printf是stdio.h里系统提供的函数,表示在标准输出设备上打印字符串
printf("hello world\n"); //printf输出默认不换行
return 0;
}
例2:手动编译C文件方法
/*
手动编译C文件:
PS *>gcc -o hello hello.c
生成一个名字为hello的可执行文件
PS *>gcc -c hello.c
生成hello.o文件,可抽取为库文件
*/
例3:system函数
//因为没有用到printf,所以没有导入stdio.h头文件
//system函数是stdlib.h头文件提供的,可以执行命令行(cmd)命令
#include <stdlib.h>
int main()
{
system("calc"); //system还可以通过绝对路径打开应用程序(exe),但有些程序可能会受到保护
return 0;
}
例4:C程序编译过程
/*
C程序编译过程:
预编译:宏定义展开,头文件展开,条件编译,删除注释,不检查语法
PS *>gcc -E hello.c -o hello.i
编译:检查语法,编译为汇编文件(参数为大S)
PS *>gcc -S hello.i -o hello.s
汇编:将汇编文件编译为目标文件(二进制文件)
PS *>gcc -c hello.s -o hello.o
链接:C程序依赖于各种库,需要把库链接到可执行程序中
PS *>gcc hello.o -o hello.exe
*/
/*
实际上可以一步到位,比如要得到汇编文件,可以直接
PS *>gcc -S hello.c -o hello.s
*/
例5:C语言关键字
/*
跟数据类型有关的关键字:
char, short, int, long, float, double, void
signed: 有符号数字,默认不写
unsigned: 无符号数字,必须写,且为正数
struct: 结构体
union: 联合体/共生体
enum: 枚举
跟存储有关的关键字(用的不多)
auto: 定义局部变量时声明,一般不写
extern, register
static: 定义静态变量
const: 定义常量
跟控制结构有关的关键字:略
其它:sizeof, typedef, volatile
*/
例6:防止exe运行一闪而过
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello world!\n");
system("pause");//防止双击运行exe文件时一闪而过,注意要在return之前,同时有导入stdlib.h头文件
return 0;
}
例7:常量定义/宏定义
#include <stdio.h>
#define PI 3.14159 //宏定义,注意结尾没有分号,建议这样定义常量
int main() {
//const float pi = 3.14159; //定义常量,这种定义不安全,但仅限于C,C++里这样是安全的
int r = 2;
//float per = 2 * pi * r;
float per = 2 * PI * r;
printf("The perimeter is %f\n", per); //%f-->浮点型,%lf-->双精度浮点型
printf("The perimeter is %.2f\n", per); //此处保留小数会四舍五入,但C++里不会,python里也不会
return 0;
}
例8:格式化占位符
#include <stdio.h>
int main() {
// int dec = 10;
// int oct = 010;
// int hex = 0x10;
//C语言不能直接输入输出二进制数(?)
signed int n = -12; //定义有符号整型,默认不写signed
unsigned int m = 12; //定义无符号整型,如果强制添加符号会溢出范围(原理从二进制看)
int a = 10;
printf("%d\n", a); //%d-->十进制整型
printf("%o\n", a); //%o-->八进制整型
printf("%x\n", a); //%x-->十六进制整型,字母小写
printf("%X\n", a); //%X-->十六进制整型,字母大写
printf("%u\n", a); //&u-->无符号十进制
short s = 10;
int i = 20;
long g = 30;
long long h = 40; //长长整型,一般不用
printf("%hd\n", s); //%hd-->短整型
printf("%d\n", i); //%d-->整型
printf("%ld\n", g); //%ld-->长整型
printf("%lld\n", h); //%lld-->长长整型
return 0;
}
例9:scanf函数(1)
#include <stdio.h>
int main() {
int a;
//scanf是stdio.h里的函数,表示标准输入
//&是一个运算符,表示取地址,&a表示变量a在内存中的地址
//即把通过scanf获得的数据存放在变量a的地址上,即把数据赋值给a
//scanf("%d", &a); //不要加换行
//scanf不安全,因为哪怕输入的数据不符合类型要求也不会报错,CLion建议用strtol
//但在后面使用scanf("%c", &ch)时没有提示,暂时不明白原理
scanf_s("%d", &a); //VS2109推荐用scanf_s
printf("%d\n", a);
return 0;
}
例10:sizeof关键字
#include <stdio.h>
int main() {
//sizeof不是一个函数,也不需要导入头文件,它能返回一个数据在内存中所占的空间,单位为字节(BYTE)
//1BYTE = 8bit
//sizeof的返回值是一个size_t类型,这是一个别名,本质为一个无符号整型
unsigned int len = sizeof(int);
printf("int: %u\n", len);
//short: 2字节
//int: 4字节
//long: Windows下为4字节,32位Linux下为4字节,64位Linux下为8字节
//long long: 8字节
//实际上整型在内存中的所占空间与操作系统有关
return 0;
}
例11:字符型(1)
#include <stdio.h>
int main() {
char ch = 'a';
printf("%c\n", ch); //%c-->字符型
printf("size: %lld\n", sizeof(ch)); //字符型大小为1字节
//以上为在windows64位操作系统下运行语句
//在32位下格式字符使用%d而非%lld,原因暂时没搞明白
return 0;
}
例12:字符型(2)
#include <stdio.h>
int main() {
char ch;
printf("Please enter a capital alpha:\n");
//scanf("%c", &ch);
scanf_s("%c", &ch, 128); //scanf_s需要两个参数,第二个指定读取位数,防止溢出
printf("The result: %c\n", ch + 32);
printf("30%%"); //输出百分号的方式
return 0;
}
例13:查看数据的内存地址
#include <stdio.h>
int main() {
float f = 3.14f;
double d = 3231.14;
int i = 10;
printf("int: %p\n", &i); //%p-->变量在内存中的地址编号
printf("float: %p\n", &f);
printf("%e\n", d); //%e-->科学计数法
return 0;
}
例14:数据类型的范围
/*
bit,b,比特,位;
Byte,BYTE,B,字节,1Byte=8bit;
WORD,双字节;
QWORD,四字节;
Kb,千位,1Kb=1024b;
KB,千字节,1KB=1024B;
Mb,兆位,1Mb=1024Kb;
MB,兆字节,1MB=1024KB;
有符号:
char类型取值范围(1字节,8位):-2^7~2^7-1
int类型取值范围(4字节,32位):-2^31~2^31-1
计算机把-0作为范围内的最小值,比如在char类型里为-128,在int类型里为-2147483648
无符号:
char类型:0~2^8-1 (0~255)
int类型:0~2^32-1
*/
例15:其它不常用关键字
#include <stdio.h>
int main() {
volatile int num;
//如果一个变量声明了但没有赋值也没有使用,在编译时会优化删掉
//添加volatile限定词可以防止优化
register int n;
//建议型指令,如果寄存器有空闲,则把变量保存在寄存器里,而非内存中
//建议不使用,因为会造成寄存器滥用
return 0;
}
例16:字符串(1)
#include <stdio.h>
int main() {
//字符串是内存中一连串的char空间,以'\0'结尾
//比如,'a'占一个字节,"a"占两个字节,因为有两个字符:'a'和'\0'
/*
* 一个"hello world"字符串实际在内存中是这样的:
* |h|e|l|l|o| |w|o|r|l|d|\0|
* */
char * b = "hello\0 world";
char c[11] = "hello world";
printf("%s\n", b); //%s-->字符串,在内存中从b开始匹配字符,遇到'\0'结束
printf("%s\n", c); //本来数组应该是c[12],此处人为地把最后一位'\0'挤掉,会多输出一些奇怪的东西
return 0;
}
例17:字符串(2)
#include <stdio.h>
int main() {
int a = 10;
printf("===%5d===\n", a); //规定宽度,如果原数据的长度大于规定的宽度,则按原数据长度输出
printf("===%-5d===\n", a); //没有“-”默认右对齐,加“-”左对齐
printf("===%05d===\n", a); //在左边填充0直到达到规定宽度,不能与“-”搭配,且不能在右边填充
float b = 3.14159f;
printf("===%7.2f===\n", b); //7表示整体宽度,.2表示保留小数位数
printf("===%-7.2f===\n", b); //可以左对齐,但一般不填充0
return 0;
}
例18:putchar函数
#include <stdio.h>
int main() {
char a = 'n';
//putchar是stdio.h里的输出函数,专门用于输出字符
putchar(a); //输出默认不换行
putchar('B');
putchar(97); //其参数默认为int,当然也可以是字符
putchar('\n');
return 0;
}
例19:scanf函数(2)
#include <stdio.h>
int main() {
int a, b;
scanf("%d,%d", &a, &b);
//需要输入多个数据时,如果scanf里的格式没有加分隔符,例如"%d%d",则默认以输入时的空格或换行作为分隔符
//如果加了分隔符,如此处的"%d,%d",则以所加分隔符分隔
//分隔符不能为'\n',因为scanf以'\n'作为输入的结尾(输入完毕后回车使程序继续运行)
//此亦强调scanf中不能有'\n'
/*
* 此外,scanf("%3d%d", &a, &b)可以对输入的整数进行宽度约束
* (不知道有啥用)*/
printf("%d\t%d\n", a, b);
return 0;
}
例20:getchar函数
#include <stdio.h>
int main() {
char c;
//getchar()是stdio.h提供函数
c = getchar(); //从输入设备获取一个字符,没有参数
//也可以写在文件最后(return之前)防止exe执行时一闪而过
putchar(c);
return 0;
}
例21:算术运算符
#include <stdio.h>
int main() {
int a = 10, b = 7;
printf("%d\n", a / b); //整除,结果为整型
printf("%d\n", a % b); //不能用浮点数取余,但python中可以
return 0;
}
例22:类型转换
/*
类型转换:
short,char < signed int < unsigned int < long < double
float < double
*/
例23:逻辑运算符
#include <stdio.h>
int main() {
/*
* C语言中用数字1表示真,数字0表示假,而非true和false
* */
int a = 10, b = 20;
printf("%d\n", a*2 == ++b);
printf("%d\n", b);
return 0;
}
例24:goto跳转结构
#include <stdio.h>
int main() {
printf("a\n");
printf("b\n");
goto FLAG; //无条件跳转,定义了标签后可以随意跳转,但是尽量不要在函数间跳转;也可以造成死循环
printf("c\n");
printf("d\n");
FLAG: //注意这里是冒号
printf("e\n");
printf("f\n");
return 0;
}
例25:数组(1)
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5}; //数组的定义方式
printf("%p\n", arr); //数组名是一个指向数组首地址(首元素的地址)的地址常量
for (int i = 0; i < 5; ++i) {
printf("%p\n", &arr[i]);
}
printf("size: %lld\n", sizeof(arr)); //单个数据大小×个数(不管是否真的有这么多)
return 0;
}
例26:数组(2)
#include <stdio.h>
#define SIZE 5
int main() {
int array[] = {1,2,3,4}; //不预先设定长度,按实际长度开辟空间
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
printf("%d\n", array[i]);
}
// const int j = 5; //这样子貌似不行,难道这就是不安全?
int a[SIZE] = {}; //数组的大小必须预先设定,且必须为常量(不可改)
for (int k = 0; k < SIZE; ++k) {
scanf("%d", &a[k]); //输入可以为“10 20 30 40 50”这种格式,默认以空格和换行分隔
}
for (int m = 0; m < SIZE; ++m) {
printf("%d\n", a[m]);
}
return 0;
}
例27:冒泡排序
#include <stdio.h>
#define SIZE 6
int main() {
int arr[SIZE] = {27, 34, 13, 54, 6, 20};
//for (int i = SIZE - 1; i > 0; --i) {
// for (int j = 0; j < i; ++j) {
// if (arr[j] > arr[j + 1]) {
// int temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
// }
// }
//}
for (int i = 0; i < SIZE - 1; ++i) {
for (int j = 0; j < SIZE - 1 - i; ++j) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
for (int k = 0; k < SIZE; ++k) {
printf("%d ", arr[k]);
}
return 0;
}
例28:二维数组
#include <stdio.h>
int main() {
int d_array[3][4] = { //二维数组定义方法
{1, 2, 3, 10},
{4, 5, 6, 20},
{7, 8, 9, 30}
};
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
printf("%d ", d_array[i][j]);
}
}
printf("\ntotal size: %lld\n", sizeof(d_array)); //大小=行*列*单个数据大小
printf("row: %lld\n", sizeof(d_array) / sizeof(d_array[0])); //行数
printf("column: %lld\n", sizeof(d_array[0]) / sizeof(d_array[0][0])); //列数
//首地址
printf("%p\n", d_array);
printf("%p\n", d_array[0]); //这依然是个数组
printf("%p\n", &d_array[0][0]); //这是个元素,所以要&
printf("%p\n", d_array[1]); //第二行首地址,跟第一行差了16位
return 0;
}
例29:定义字符串
#include <stdio.h>
int main() {
char a[5] = {'h', 'e', 'l', 'l', 'o'}; //定义字符数组,大小为5
char * b = "hello"; //定义字符串,大小为6,因为字符串以'\0'结尾
char c[] = {'h', 'e', 'l', 'l', 'o', '\0'}; //等同于字符串b
char d[6] = {'h', 'e', 'l', 'l', 'o'}; //等同于字符串b,因为空位用0补齐
//整型0等同于'\0',但不等同于'0'
printf("%s\n", a); //这里在数组内找不到'\0',所以按理还会向后输出一下乱码
printf("%s\n", b); //下面这些则不会,因为都能找到'\0'
printf("%s\n", c);
printf("%s\n", d);
//C语言中没有字符串这种数据类型,一般把以'\0'结尾的字符数组作为字符串
//所以字符串是一种特殊的字符数组
return 0;
}
例30:字符串的相关问题
#include <stdio.h>
int main() {
char s[10];
scanf("%s", s); //这里没有&,是因为s本身就是一个地址,表示字符数组的首地址
/*
* 这里需要注意的问题:
* 1、s的可获取字符数为9个,因为还要预留一个给'\0';
* (但是测试的时候好像超了也不报错而且也能获取到多余的字符)
* 2、如果在输入字符中有空格,则空格之后的字符都不会被获取,因为scanf以空格或换行为结束
* */
//在使用scanf_s时,第二个参数一定要与字符串定义长度相同,测试如此
//此例,scanf_s("%s", s, 10);
printf("%s\n", s);
char str[] = {"hello"}; //这其实是一个字符串
char string[][6] = {"hello", "world"};
//这是一个字符串数组,但要注意第二维定义的数要比字符串中最长的那个还要长一点
return 0;
}
例31:获取字符串
#include <stdio.h>
int main() {
char str[100];
//gets()是stdio.h提供的输入函数
gets(str); //从标准输入设备中获取字符串,它允许字符串中含有空格
//但它依然无法识别输入的字符有多少,所以可能会溢出
printf("%s\n", str);
/*scanf("%[^\n]", str); //实际上这样也可以获取到输入的空格,但这样有别的缺陷
* 里边是一个正则表达式,所以scanf和printf函数实际上是格式化输入输出函数
* f代表format
* */
char string[100];
//fgets()是stdio.h提供的更安全的输入函数
//它允许输入中有空格,且在第二个参数规定了接受字符串的最大量,所以不会溢出
//第三个参数是固定的,是系统规定的输入流常量
//它会把用户输入的最后一个换行也作为接受的字符,
//所以在输入字符串不大于第二个参数的情况下,接受的字符串结尾有'\n'
//如果输入字符串大于规定大小了,自然就不会接收到'\n'了,
//但无论如何最后一定是'\0'结尾
fgets(string, sizeof(string), stdin);
return 0;
}
例32:打印字符串
#include <stdio.h>
int main() {
char str[] = "hello world";
//puts()是stdio.h提供的字符串输出函数
puts(str); //输出默认带换行,只能输出字符串
puts("hello\0world"); //依然会遇到'\0'停止
//fputs()是stdio.h提供的字符串输出函数
fputs(str, stdout); //输出不带换行
//实际上fgets()和fputs()函数主要用于读写文件,其中f代表file
return 0;
}
例33:字符串长度
#include <stdio.h>
#include <string.h>
int main() {
char str[100] = "hello world";
printf("数组大小:%lld\n", sizeof(str));
//strlen()是string.h提供的函数
//返回字符串大小,但不包括'\0'
printf("字符串大小:%lld\n", strlen(str));
return 0;
}
例34:获取随机数
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int main() {
//time()是time.h提供的函数
//time_t实际是一个长长整型,time()返回的是一个以秒为单位的数字(1970.1.1至今的秒数)
time_t timer = time(NULL);
//srand()设定种子,由相同的种子得到的伪随机数是相同的,大概同python下的seed(0)
//size_t实际是一个无符号长长整型,这里强制转换数据类型
//srand里的s大概是seed的意思
//所以这里如果只是想设置一个种子的话,写srand(0)也是可以的
srand((size_t)timer);
int r = rand(); //生成随机数
printf("%d\n", r);
return EXIT_SUCCESS; //stdlib.h提供的宏,实际值就是0
}
例35:限定随机数范围
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int main() {
srand((size_t)time(NULL));
for (int i = 0; i < 5; ++i) {
printf("%d ", rand() % 100); //限定随机数范围在0~99
}
printf("\n");
for (int j = 0; j < 5; ++j) {
printf("%d ", rand() % 51 + 50); //限定随机数在50~100
//取余后限定在0~50,加偏移量后限定在50~100
}
return EXIT_SUCCESS;
}
例36:随机双色球
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define REDS 6 //规定红球个数
int main() {
//红色球6个,1~32,蓝色球1个,1~16
srand((size_t)time(NULL));
int red_balls[REDS] = {0}; //手动初始化为6个0,防止自动初始化成一些乱七八糟的数
int flag = 0; //设立一个判断红球是否有重复的布尔值
//开始选红球
int i = 0;
while (i < REDS) {
int red_v = rand() % 32 + 1; //限定范围
for (int j = 0; j < REDS; ++j) {
if (red_v == red_balls[j]) {
flag = 1;
break;
}
}
if (flag == 0) {
red_balls[i] = red_v;
i++;
}
flag = 0;
}
//选完红球
//开始选蓝球
int blue_v = rand() % 16 + 1;
//选完蓝球
//打印
for (int k = 0; k < REDS; ++k) {
printf("%d ", red_balls[k]);
}
printf("\n%d\n", blue_v);
return EXIT_SUCCESS;
}
例37:函数定义
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
/**
* 只要函数没调用,则形参不占存储空间,所以形参不能赋值
* 但是python的形参是可以附加默认值的
* @param a 一个数
* @param b 另一个数
*/
void add_p(int a, int b) {
int sum_t = a + b;
printf("%d + %d = %d\n", a, b, sum_t);
}
int main() {
int s = add(2, 4);
printf("sum: %d\n", s);
add_p(2, 5);
//add_p和add都是函数指针(不加括号的情况)
//返回值不能返回多个值
//如果实际返回值类型与函数前规定的返回值类型不符,则强行转换为函数前规定的返回值类型
//如果不能强制转换,就报错
return 0;
}
例38:声明函数
#include <stdio.h>
/*
* 函数使用的三个步骤:函数声明、函数定义、函数调用
* 如果函数在主调函数(main函数)之前定义,则不需要声明
* 如果在main之后定义,则需要先在之前声明
* 声明可以多次
* 以下是三种声明方式(实际上后两步声明冗余)*/
void add_p(int a, int b);
void add_p(int, int);
extern void add_p(int a, int b);
/*
* 广义上讲,声明中包含定义,定义是声明的一个特例,但并非所有声明都是定义
* 比如:
* int b;
* 既是一个声明也是一个定义
* 但
* extern int b;
* 就只是一个声明
* 一般来讲,声明不需要建立存储空间,只要有返回值类型,名称,形参类型、个数和顺序就可以了
* 但定义需要建立存储空间,需要有完整的函数主体
* */
int main() {
int a = 10;
int b = 20;
add_p(a, b);
return 0;
}
void add_p(int a, int b) {
int sum_t = a + b;
printf("%d + %d = %d\n", a, b, sum_t);
}
例39:exit函数
#include <stdio.h>
#include <stdlib.h>
void kill_y() {
printf("You are dead.");
//exit()是stdlib.h提供的函数,可以直接结束整个程序
exit(0);
}
int main() {
kill_y();
printf("After killed"); //不会打印
return 0;
}
例40:多文件编译
//jmath.h文件下
#ifndef TESTPROJ_JMATH_H //如果没有定义过这个宏
#define TESTPROJ_JMATH_H //就定义这个宏,防止头文件重复包含(你中有我我中有你)
int add_t(int a, int b); //函数或全局变量声明
int mul_t(int a, int b);
#endif //TESTPROJ_JMATH_H
//jmath.c文件下
int add_t(int a, int b) { //函数的具体实现
return a + b;
}
int mul_t(int a, int b) {
return a * b;
}
//main.c文件下
#include <stdio.h>
#include "jmath.h" //导入自定义的文件
int main() {
int c = add_t(2, 3);
printf("%d\n", c);
int d = mul_t(3, 4);
printf("%d\n", d);
return 0;
}
//在终端使用gcc时这样编译
//PS *>gcc main.c jmath.c jmath.h -o main.exe
例41:指针的定义和使用
#include <stdio.h>
int main() {
int a = 10;
//int*是一种数据类型,存储地址编号,*之前的数据类型取决于要存的地址处是什么数据类型
//比如也可以是char*什么的
int* p = &a; //取出a的地址,赋给p
printf("%p\n", &a);
printf("%p\n", p);
//此处的*是取值运算符,所以*p表示的是p这个地址上的值,实际上就是a
//这是通过指针间接修改变量的值
int b = 19;
int c = 20;
*p = 100;
printf("%d\n", a);
printf("%d\n", *p);
//p的值是0x000000000061FE14
//64位操作系统,所以有8个字节:00 00 00 00 00 61 FE 14
//地址都是无符号整型,64位系统给p开辟的空间是8个字节(32位是4个字节)
printf("%lld\n", sizeof(p));
return 0;
}
例42:指针注意事项
#include <stdio.h>
int main()
{
char c = 'a';
int* p = &c;
printf("%d\n", c); //输出97
printf("%d\n", *p); //输出-858993567
//在定义指针时一定要与取址的数据类型对应上
//此处的错误在于,char类型只占1个字节,而用*p根据地址取值时,因为定义的是int*,int类型占4个字节,所以*p会从第一个地址后连续再取三个,从而输出一些奇怪的东西
return 0;
}
例43:野指针
#include <stdio.h>
int main()
{
/* 这是一个野指针
* 指针也是变量,是变量就可以赋值,只要不超出范围
* 如果我们直接给指针变量指定一个值,这个指针就变成了一个野指针
* 程序允许野指针存在,但野指针可能指向一个我们并不知道有什么的区域
* 所以在操作野指针指向的内存区域时可能会报错
* */
int* p = 0x00ff722c;
printf("%d\n", *p);
return 0;
}
例44:空指针
#include <stdio.h>
int main()
{
/* 这是一个空指针
* 空指针指向地址编号为0的空间
* 因为地址0-255的区域为系统专用,不允许访问
* 所以任何对空指针的操作(读取、写入)都会报错
* 空指针一般用于条件判断,比如对一块地址取址失败后,指针变量会变为0
* 因此可以通过判断一个指针是不是空指针来判断取址是否失败
* */
int* p = NULL;
return 0;
}
例45:万能指针
#include <stdio.h>
int main()
{
/* 万能指针void*并非真的万能
* 它可以接受任意类型的变量的地址
* 但是没办法通过void*类型的指针变量进行寻址
* 因为寻址前会根据指针类型判断寻址个数,比如char*会寻1个字节,int*会寻4个字节
* 但void本身并没有大小,它不能用sizeof取大小,所有void*也无法判断寻址个数
* 所以在寻址时需要转换数据类型
* */
int a = 10;
void* p = &a;
//*p = 100; 这样是不行的
*(int*)p = 100; //在已知a是int类型时,可以这样转换
printf("%d\n", a);
//虽然sizeof(void)不行,但sizeof(void*)可以,因为所有的指针变量大小都是一样的,
//在32位下为4个字节,64位下为8个字节
return 0;
}
例46:const和指针
#include <stdio.h>
int main()
{
const int a = 10;
//a = 100; 常量不可修改
int* p = &a;
*p = 100; //这样就可以修改了,所以const定义的常量是不安全的
printf("%d\n", a);
int b = 20;
int c = 30;
const int* q = &b;
//*q = 200; 此处依然不可改
q = &c; //此处不报错,修改成功,所以此处实际约束的是*q而非q
printf("%d\n", *q);
int* const r = &b;
//r = &c; 这样不行
*r = c; //修改成功,所以此处实际约束的是r而非*r
printf("%d\n", *r);
//所以const约束就近
//甚至可以这么写
const int* const s = &b; //但这样就没办法修改了吗?不是的
//定义一个二级指针
int** t = &s;
//此时s是一个一级指针,t是一个二级指针,s存的是一个内存地址,t存的是存这个地址的内存地址
**t = c;
printf("%d\n", *s); //修改成功
return 0;
}
例47:指针和数组(1)
#include <stdio.h>
int main()
{
//字符型本身也可以转为整型,所以不报错
int arr[10] = { 1, 2, 3, 'a', 'b' };
//数组名是一个指向数组第一项的地址,所以可以理解为一个指针(此处为int*)
printf("%p\n", arr);
//指针变量+1时,其地址会偏移相应的指针地址所存数据的大小
//int* p = 地址
//p + 1 == 地址 + sizeof(int)
printf("%d\n", *(arr + 1)); //输出2
printf("%p\n", arr + 1); //比上一个地址多4
char ch[5] = { 'a', 'b', 'c', 'd' };
printf("%p\n", ch);
printf("%p\n", ch + 1); //比上一个地址多1
printf("%c\n", *(ch + 1)); //输出'b'
return 0;
}
例48:指针和数组(2)
#include <stdio.h>
int main()
{
int arr[10] = { 1, 2, 3, 'a', 'b' };
int* p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d\n", arr[i]);
printf("%d\n", *(p + i)); //同上,所以i其实就是偏移量
}
for (int j = 0; j < 5; j++)
{
printf("%d\n", *p++); //先寻址,再自增
}
int b = p - arr; //arr是常量不会变,因为数组大小定义为10,所以p指向了数组第六个值
printf("%d\n", b); //输出5,指针变量相减,结果是偏移量,所有指针类型相减都是int
return 0;
}
例49:指针和数组(3)
#include <stdio.h>
void func(int array[])
{
printf("%d\n", sizeof(array));
}
int main()
{
int arr[10] = { 1, 2, 3, 'a', 'b' };
//p和arr的不同之处
int* p = arr;
printf("%d\n", sizeof(p)); //变量,输出4
printf("%d\n", sizeof(arr)); //常量,输出40,因为数组大小定义为10
func(arr); //而此处输出为4,因为数组名在作为函数参数传递的时候退化为指针了
//因此对于func函数,定义参数int* array效果是等同的,
//在函数内部调用数组元素实际上也是使用了指针偏移量,即p[1]等同于arr[1]
return 0;
}
例50:字符串复制(1)
#include <stdio.h>
/**
* 虽然传递的实参是字符数组,但设置的形参为指针,
* 因为都一样,数组会退化为指针,而且用指针操作数组与用数组名操作等同
* while (src[i] != '\0')等同于while (src[i])
* 最后一步是要在字符串最后添加一个'\0',因为字符串都要以'\0'结尾
* '\0'等同于0
* */
void str_cp(char* tar, char* src)
{
int i = 0;
while (src[i] != '\0')
{
tar[i] = src[i];
i++;
}
tar[i] = '\0';
}
int main()
{
char s[] = "Hello World";
char t[20];
str_cp(t, s);
printf("%s\n", t);
return 0;
}
例51:字符串复制(2)
/**
* 此处两个函数与上例的函数等同
* 此处两个函数都是用指针偏移量操作数组元素
* 此处偏移量都是sizeof(char)
* */
void str_cp2(char* tar, char* src)
{
int i = 0;
while (*(src + i))
{
*(tar + i) = *(src + i);
i++;
}
*(tar + i) = 0;
}
void str_cp3(char* tar, char* src)
{
while (*src)
{
*tar = *src;
tar++;
src++;
}
*tar = 0;
}
/**
* 分步如下:
* 取值,*tar,*src
* 赋值,*tar=*src
* 判断,*tar是否为0
* 自增,tar++,src++
* 所以此处是先赋值后判断,最后的0已经赋上,不需要再独自添加
* */
void str_cp4(char* tar, char* src)
{
while (*tar++ = *src++);
}
例52:指针运算
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int* p;
char* q;
p = &arr[3];
q = &arr[3];
printf("%p\n", p);
printf("%p\n", q);
for (int i = 0; i < 3; i++)
{
p--;
q--;
}
printf("%p\n", p); //减少了12
printf("%p\n", q); //减少了3,因为指针计算时偏移量跟定义的类型有关,但赋值时无所谓,因为都是一个地址
return 0;
//除此之外,指针之间可以相减(见例48),但不能相加,乘除也不行
//指针可以加减偏移量,但乘除取余都不行
//可以判断大小,可以逻辑与或运算
}
例53:指针和数组(4)
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int* p;
p = &arr[3];
printf("%d\n", p[-2]);
//arr[-2]是不允许的,数组索引中不能有负值,此处跟python不同
//但使用指针变量操作数组时就可以有负值了,即负偏移量
printf("%d\n", p[-5]); //但是这样就超出了范围,不报错但没意义
return 0;
}