第一部分我们整理了C语言的基本特点、环境、语法等详情可戳👉紫薇星上的C语言(1)
上一部分我们整理了C语言的运算符、判断、循环与数组等详情可戳👉紫薇星上的C语言(2)
这一部分我们会整理C语言的指针、字符串等知识点,涉及到指针会比较难,但是大家不要怕,微笑着面对它,奥里给!
11.输入与输出
当我们提到输入时,这意味着要向程序填充一些数据,输入可以是以文件的形式或从命令行中进行,C 语言提供了一系列内置的函数来读取给定的输入,并根据需要填充到程序中。
当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据,C 语言提供了一系列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。
C 语言把所有的设备都当作文件,所以设备(比如显示器)被处理的方式与文件相同,以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 屏幕 |
文件指针是访问文件的方式,这一小节将讲解如何从屏幕读取值以及如何把结果输出到屏幕上,C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。scanf() 函数用于从标准输入(键盘)读取并格式化,printf() 函数发送格式化输出到标准输出(屏幕)。
- int scanf(const char *format, ...) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。
- int printf(const char *format, ...) 函数把输出写入到标准输出流 stdout ,并根据提供的 format 产生输出。
- format 可以是一个简单的常量字符串,但可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数,还有许多其他可用的格式选项,可以根据需要使用。
示例:
#include <stdio.h> // 执行 printf() 函数需要该库
int main()
{
char str[100];
int i;
printf( "Enter a value :");//显示引号中的内容
scanf("%s %d", str, &i);//输入一串字符给str数组,输入一个整型数字给i
printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}
加入我们输入HelloWorld! 123,那么程序输出结果为:
Enter a value :HelloWorld! 123
You entered: HelloWorld! 123
需要注意的是:
- 所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
- printf() 用于格式化输出到屏幕。printf() 函数在 "stdio.h" 头文件中声明。
- stdio.h 是一个头文件 (标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数与scanf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。
- scanf() 期待输入的格式与给出的 %s 和 %d 相同,这意味着必须提供有效的输入,比如 "string integer",如果提供的是 "string string" 或 "integer integer",它会被认为是错误的输入。另外,在读取字符串时只要遇到一个空格,scanf() 就会停止读取,所以 "this is test" 对 scanf() 来说是三个字符串。
- return 0; 语句用于表示退出程序。
除了printf() 函数与scanf() 函数,输入输出还可以使用:
- getchar() & putchar() 函数
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
示例:
#include <stdio.h>
int main( )
{
int c;
printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");
return 0;
}
当上面的代码被编译和执行时,它会等待输入一些文本,当输入一个文本并按下回车键时,程序会继续并只会读取一个单一的字符,例如我们输入HelloWorld!,显示如下:
Enter a value :HelloWorld!
You entered: H
- gets() & puts() 函数
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。
示例:
#include <stdio.h>
int main( )
{
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}
当上面的代码被编译和执行时,它会等待输入一些文本,当输入一个文本并按下回车键时,程序会继续并读取一整行直到该行结束,例如我们输入HelloWorld!,显示如下:
Enter a value :HelloWorld!
You entered: HelloWorld!
12.指针
虽然一直流传指针是C语言中最难的部分,但是不用害怕它,如果一遍没有看懂,再多看几遍就好啦!因为指针很重要,所以这部分我会写的特别详细,大家加油!
12.1指针的基本概念
- 什么是指针
指针是一个值为内存地址的变量(或数据对象)。每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号 & 运算符访问的地址,它表示了在内存中的一个地址。简单来说,对于一个变量,在它前面加 & 后再输出的是它的内存地址,如:
#include <stdio.h>
int main ()
{
int var1 = 1;
char var2[10];
printf("var1 变量: %d\n", var1 );
printf("var1 变量的地址: %p\n", &var1 );
printf("var2 变量的地址: %p\n", &var2 );
return 0;
}
这段代码被编译和执行后,结果如下:
var1 变量: 1
var1 变量的地址: 0x7fff5cc109d4
var2 变量的地址: 0x7fff5cc109de
可以看到,这里 var1 的值为 1,但在加了 & 后就变成了 0x7fff5cc109d4,这里的地址为定义 var1 时计算机自动分配的内存空间的地址,同样的在定义一个数组 var2 后,计算机也分配了另一个内存空间给 var2。同时输出指针的值需要使用%p,%p用来输出指针的值、输出地址符。
指针变量声明的一般形式为:
type *var-name;
type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称,用来声明指针的星号 * 与乘法中使用的星号是相同的,但是在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数,不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
- 如何使用指针
使用指针时会进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。示例:
#include <stdio.h>
int main ()
{
int var = 20; /* 实际变量的声明 */
int *ip; /* 指针变量的声明 */
ip = &var; /* 在指针变量中存储 var 的地址 */
printf("Address of var variable: %p\n", &var );
/* 在指针变量中存储的地址 */
printf("Address stored in ip variable: %p\n", ip );
/* 使用指针访问值 */
printf("Value of *ip variable: %d\n", *ip );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20
通过注释可以理解:ip为一个指针,var 为一个变量,在变量前加 & 后就可以表示它的地址,ip = &var; 这条语句表示在指针变量中存储 var 的地址,而 ip 为一个指针,那么 *ip 就表示指向 ip 所表示的地址的值,值为 ver。这里有一个小技巧,可以将 type * 整个看作一个新的类型,就不用将类型与 * 搞混。
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。示例:
#include <stdio.h>
int main (){
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
ptr 的地址是 0x0
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置,但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
12.2指针的作用
- 指针的算术运算
指针是一个用数值表示的地址,因此可以对指针执行算术运算,可以对指针进行四种算术运算:++、--、+、-。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,如果对该指针执行 ptr++ 的算术运算,在执行运算后 ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节,这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。要注意:
- 指针的每一次递增,会指向下一个元素的存储单元。
- 指针的每一次递减,会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
- 指针的比较
指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
- 指向 指针的 指针
指向 指针的 指针是一种多级间接寻址的形式,或者说是一个指针链。通常一个指针包含一个变量的地址,但这个指针本身也有自己的内存空间,当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号,当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,例如:
#include <stdio.h>
int main (){
int var;
int *ptr;
int **pptr;
var = 3000;
/* 获取 var 的地址 */
ptr = &var;
/* 使用运算符 & 获取 ptr 的地址 */
pptr = &ptr;
/* 使用 pptr 获取值 */
printf("Value of var = %d\n", var );
printf("Value available at *ptr = %d\n", *ptr );
printf("Value available at **pptr = %d\n", **pptr);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of var = 3000
Value available at *ptr = 3000
Value available at **pptr = 3000
12.3指针 与 数组 与 函数
- 指针与数组
对于数组来说,数组名是一个指向数组中第一个元素的常量指针,在这条语句中:
double balance[50];
balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此下面的代码把 p 赋值为 balance 的第一个元素的地址:
double *p;
double balance[10];
p = balance;
使用数组名作为常量指针是合法的,反之亦然,因此 *(balance + 4) 是一种访问 balance[4] 数据的合法方式,一旦把第一个元素的地址存储在 p 中,就可以使用 *p、*(p+1)、*(p+2) 等来访问数组元素。
-
数组与函数
如果要在函数中传递一个一维数组作为参数,必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,也可以传递一个多维数组作为形式参数。
形式参数是一个已定义大小的数组:
void myFunction(int param[10]){ ... }
形式参数是一个未定义大小的数组:
void myFunction(int param[]){ ... }
形式参数是一个指针:
void myFunction(int *param){ ... }
C 语言不允许返回一个完整的数组作为函数的参数,但可以通过指定不带索引的数组名来返回一个指向数组的指针,如果想要从函数返回一个一维数组,必须声明一个返回指针的函数,如:
int * myFunction(){ ... }
另外,C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。我们来举一个例子,下面这段代码会产生10个随机数,并使用数组来返回他们:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/* 要生成和返回随机数的函数 */
int * getRandom( ){
static int r[10];
int i;
/* 设置种子 */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i){
r[i] = rand();
printf( "r[%d] = %d\n", i, r[i]);
}
return r;
}
/* 要调用上面定义函数的主函数 */
int main ()
{
/* 一个指向整数的指针 */
int *p;
int i;
p = getRandom();
for ( i = 0; i < 10; i++ ){
printf( "*(p + %d) : %d\n", i, *(p + i));
}
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
r[0] = 313959809
r[1] = 1759055877
r[2] = 1113101911
r[3] = 2133832223
r[4] = 2073354073
r[5] = 167288147
r[6] = 1827471542
r[7] = 834791014
r[8] = 1901409888
r[9] = 1990469526
*(p + 0) : 313959809
*(p + 1) : 1759055877
*(p + 2) : 1113101911
*(p + 3) : 2133832223
*(p + 4) : 2073354073
*(p + 5) : 167288147
*(p + 6) : 1827471542
*(p + 7) : 834791014
*(p + 8) : 1901409888
*(p + 9) : 1990469526
- 指针与函数
函数指针是指向函数的指针变量,通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数,函数指针变量的声明格式:
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
示例:
#include <stdio.h>
int max(int x, int y){
return x > y ? x : y;
}
int main(void){
/* p 是函数指针 */
int (* p)(int, int) = & max; // &可以省略
int a, b, c, d;
printf("请输入三个数字:");
scanf("%d %d %d", & a, & b, & c);
/* 与直接调用函数等价,d = max(max(a, b), c) */
d = p(p(a, b), c);
printf("最大的数字是: %d\n", d);
return 0;
}
输入1 2 3,当上面的代码被编译和执行时,它会产生下列结果:
请输入三个数字:1 2 3
最大的数字是: 3
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数,简单讲回调函数就是由别人的函数执行时调用你实现的函数。
我们举一个例子,在示例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。示例中我们定义了回调函数 getNextRandomValue,它返回一个随机值,作为一个函数指针传递给 populate_array 函数,populate_array 函数将调用 10 次回调函数,并将回调函数的返回值赋值给数组。示例:
#include <stdlib.h>
#include <stdio.h>
// 回调函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void)){
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
// 获取随机值
int getNextRandomValue(void){
return rand();
}
int main(void){
int myarray[10];
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
16807 282475249 1622650073 984943658 1144108930 470211272 101027544 1457850878 1458777923 2007237709
13.字符串
字符串实际上是使用 null 字符 '\0' 终止的一维字符数组,因此一个以 null 结尾的字符串,包含了组成字符串的字符。下面的代码声明和初始化创建了一个 "Hello" 字符串,由于在数组的末尾存储了空字符,所以字符数组的大小比单词 "Hello" 的字符数多一个:
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
依据数组初始化规则,您可以把上面的语句写成以下语句:
char greeting[] = "Hello";
其实不需要把null放在字符串常量的末尾,编译器会在初始化数组时自动把 '\0' 放在字符串的末尾,输出上面的字符串:
#include <stdio.h>
int main (){
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("Greeting message: %s\n", greeting );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Greeting message: Hello
C语言中有大量的操作字符串的函数:
序号 | 函数 & 目的 |
---|---|
1 | strcpy(s1, s2); 复制字符串 s2 到字符串 s1。 |
2 | strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。 |
3 | strlen(s1); 返回字符串 s1 的长度。 |
4 | strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。 |
5 | strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
6 | strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
今天写的比较少,因为指针很重要,所以这一部分主要都是在写指针。
下一部分将会对结构体、共用体、文件读写等知识点进行整理,下次见👋