环境:CLion2021.3;64位macOS Big Sur
函数的返回值:希望得到的数据的类型。在自定义函数中,若不写返回值,默认为int。
形式参数:函数定义的时候括号内的参数为形式参数。形参只有在函数被调用时才会为其分配空间,函数调用结束时自动销毁,即形参的生命周期在函数内部,这一点与局部变量相同。
实际参数:程序运行时实际传递给函数的参数为实际参数。
函数内复合语句中定义的变量只在复合语句内有效(复合语句指一对大括号)
C语言提供的函数,可以查询C语言API了解,这里不赘述。
提供两个C语言的API地址:
1.https://cplusplus.com/reference/cstdio/
2.https://en.cppreference.com/w/
注意:
1.函数不能嵌套定义,但是可以嵌套调用;
2.传值调用和传址调用的区别:
传值调用:将实参的值传递给形参,二者没有直接的联系,函数无法改变实参的值。
传址调用:将实参的地址传递给形参,二者有直接的联系,函数可以改变实参的值。
举个例子,定义一个函数交换a,b的值:
void swap1(int x,int y)//仅仅传递参数的值,为传值调用
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void swap2(int *pa, int *pb)//传递了参数的地址,为传址调用
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
主函数:
int a = 10;
int b = 20;
printf("before:a=%d,b=%d\n",a,b);
swap1(a, b);//传值调用,形参与实参没有联系,形参改变不会影响实参
printf("传值调用后:a=%d,b=%d\n",a,b);
swap2(&a,&b);//传址调用,形参与实参有联系,形参改变可以影响实参
printf("传址调用后:a=%d,b=%d\n",a,b);
分析:swap1()实际上是为形参x和y新开辟了两块空间,然后将a,b的值分别赋给x和y,在函数体内操作的是x和y,函数外的实参a和b并没有改变,所以打印结果和调用前一样。
如下图,若蓝色表示函数swap()的内部,可以看到,a、b只是把值传给了x和y而已,但在其内部操作的一直是x,y、tmp这三个变量。
而swap2()将a和b的地址作为参数,其内部虽然也为pa和pb开辟了空间,但是他们存储的是a和b的地址而非简单的存储a和b的值,这样就建立了形参和实参之间的联系,实现了在函数内改变实参的值。
1.简单说明一下函数的嵌套调用:其实我们一直在使用函数的嵌套调用,你写程序总得写int main()这个函数吧,在这个函数体内你会写printf()吧,这其实就是函数的调用,和套娃一样,比较简单,就不举例了。
2.链式访问:其实就是一个函数的返回值作为另一个函数的参数。
//printf的返回值是打印了多少个字符
printf("%d", printf("%d", printf("%s","sd1 2")));
// 1 2 3
运行结果:sd1 251
分析:3处的printf()打印了"sd1 2"共5个字符(1和2中间有个空格),因此返回值为5;
2处的printf()接受了3处的返回值5,打印在了屏幕上,此时打印了一个字符,因此返回1;
1处的printf()接受了2处的返回值1,将1打印在了屏幕上。
函数的声明其实就是告诉编译器有这个函数,包括函数名,返回值类型,参数个数和类型。
编译器编译代码的时候是从上向下扫描的,如果你想要调用的函数定义在了主函数后边,或者你压根还没写这个函数,只是知道一定得用这个函数,那么当他扫描到主函数内的调用这个函数的地方,会找不到这个函数,因此在主函数中函数调用之前就要声明有这个函数,即先声明后使用。
实际上函数声明一般放在.h头文件中,需要的时候用include引用即可;而函数的定义实现放在.c文件中
int a = 10;
int b = 20;
int Add(int, int);//函数声明
printf("%d", Add(a, b));
递归的两个必要条件:
1.存在限制条件,不满足限制条件时,递归便不再继续。
2.每次递归调用后越来越接近这个限制条件。
写递归的时候注意两点:
1.不能死递归,要有跳出条件,每次递归接近跳出条件。
2.递归层次不能太深。(函数调用时会在栈其为开辟一片空间来存储函数的临时变量,叫栈帧空间,若层次太深,栈空间可能不够用而导致栈溢出stack overflow)
1.不允许创建变量模拟实现strlen()
int myStrlen(char *a)
{
if ('\0' != *a)
return 1 + myStrlen(a + 1);
else
return 0;
}
2.求n的阶乘
int jieCheng(int n)
{
if(1 < n)
return n * jieCheng(n - 1);
else
return 1;
}
3.求第n个斐波那契数
//递归实现,效率低
int fib(int n)
{
if(2 > n)
return 1;
else
return fib(n-1) + fib(n-2); //效率很低,因为有大量重复计算的值
}
//非递归
int fib(int n)
{
int a = 1;
int b = 1;
int sum= 1;
while(2 < n)
{
sum = a + b;
a = b;
b = sum;
--n;
}
return sum;
}
4.倒序字符串
void reverseStr(char *a)
{
int len = strlen(a);
char tmp = *a;
*a = *(a + len - 1);
*(a + len - 1) = '\0';
if(strlen(a + 1) >= 2)
{
reverseStr(a + 1);
}
*(a + len - 1) = tmp;
}
5.汉诺塔,专门写了一篇汉诺塔的详细说明:最强汉诺塔解析!
函数 | 如果他的参数符合下列条件就返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v' |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
isalpha | 字母a~z或A~Z |
isalnum | 字母或者数字,a~z,A~Z,0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
if (strlen("abc") - strlen("abcdef") > 0)
printf(">");//打印此行:3 - 6 = -3,但是-3被当作无符号数处理,此时-3的补码被当作原码处理,是一个很大的数字(4294967293)。
//无符号数 +- 无符号数 = 无符号数 >= 0
else
printf("<=");
2.长度不受限制的字符串函数strcpy、strcat、strcmp
2.1 拷贝字符串 char * strcpy ( char * destination, const char * source )
(1)源字符串需要包含\0,会将’\0’一同拷贝过去
(2)目标字符串的空间要足够大
(3)目标空间必须可变
char arr[20] = "##########";
//arr = "hello";//err arr是一个地址常量,也就是一个编号,因此不能放入arr中,而应该存放到一个变量空间中。
char* p = "hello";//字符串传递的是首地址
char arr2[] = { 'a','b','c' };
strcpy(arr, arr2);//err,数组arr2中没有'\0',找不到结束标志,会一直拷贝直到遇到\0
strcpy(arr, "hello");//遇到'\0',会将'\0'一同拷贝过去,然后中止拷贝
char arr[5] = "####";
char* p = "hello world";
strcpy(arr, p);//会全部拷贝过去,但是程序会崩溃,因为越界了
printf("%s", arr);//hello world 但是程序崩溃
char* str = "xxxxxxx";
char* p = "hello world";
strcpy(str, p);//目标空间是常量字符串,不可变,程序崩溃
printf("%s", str);//无法打印
//注意str和数组名的区别,数组名虽然也是地址,但是它所指向的空间是可变的;而str指向的值常量字符串,不可变。
一个小测试,证明字符串代表的就是首元素地址:
printf("%c","123456"[3]);//4 说明"" 表示的其实就是首元素的地址
2.2 追加字符串 char * strcat ( char * destination, const char * source )
(1)目标空间要足够
(2)需要\0来判断结束,且自己的\0会被带过去
(3)会覆盖目标空间的\0
(4)目标空间要可修改
char arr1[20] = "hello \0##########";
char arr2[] = "world";
strcat(arr1, arr2);//字符串追加(连接)
printf("%s", arr1);//hello world
模拟实现strcat:
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.3
注意,此函数不能自己追加自己,因为本身的\0会被不断覆盖,陷入死循环
2.3 字符串比较 int strcmp ( const char * str1, const char * str2 )
(1)比较对应位置的ASCLL码值
(2)遇到\0停止比较
(3)字符串不能用< > =比较,因为字符串代表首元素地址,而地址是随机分配的,比较没有意义
char* p = "obc";
char* q = "abcdef";
printf("%d\n", strcmp(p, q));// 1
printf("%d\n", strcmp("abc", "abcdef"));// -1
模拟实现strcmp :
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.4
3.长度受限制的字符串函数介绍:strncpy、strncat、strncmp
3.1 char * strncpy ( char * destination, const char * source, size_t num )
char arr1[20] = "abcdefghi";
char arr2[] = "qwer";
strncpy(arr1, arr2, 6);//不够的用\0补
printf("%s", arr1);//qwer
3.2 char * strncat ( char * destination, const char * source, size_t num )
char arr1[20] = "hello ";
char arr2[] = "world";
strncat(arr1, arr2, 2);//遇到\0就结束追加,结束时也会加上\0
printf("%s", arr1);//hello wo
3.3 int strncmp ( const char * str1, const char * str2, size_t num )
char arr1[20] = "abcde";
char arr2[] = "abcdefgh";
printf("%d", strncmp(arr1, arr2, 3));//0 比较给定个数
4.字符串查找strstr、strtok
4.1 char * strstr (char * str1, const char * str2 )
找子串,若有返回第一次出现的地址;找不到返回空指针
char arr1[] = "abbbcdefghijk";
char arr2[] = "bbc";
char* ret = strstr(arr1, arr2);//在arr1中找arr2
if (ret == NULL)
printf("没找到\n");
else
printf("找到了,%s\n", ret);//找到了,bbcdefghijk
模拟实现strstr:
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.5
4.2 char * strtok ( char * str, const char * delimiters )
切割字符串,会改变被切割字符串的内容
strtok第一个参数不是NULL(一般用于第一次调用):从传入的位置开始找,找到标记后将标记改为\0,并记录这个位置
strtok第一个参数是NULL(一般用于除第一次调用以外):从上一个记录的位置开始找,找到标记后将标记改为\0,并记录这个位置
返回被分割出的token的首地址。
若delimiters中没有分隔符,则返回当前str的指针。
char* strtok(char* str,const char* sep)
char arr[] = "[email protected]";
char* p = "@.";
char tmp[20] = { 0 };//防止被切割字符串被改变
strcpy(tmp, arr);
char* ret = NULL;
for (ret = strtok(tmp, p); ret != NULL; ret = strtok(NULL, p))
printf("%s\n", ret);
5.错误信息报告 char * strerror ( int errnum )
在调用库函数失败的时候,都会设置错误码,此函数将错误码翻译成错误信息保存在了字符串中,然后返回这个字符串的首地址。
c语言有一个全局变量来保存错误码:int errno;
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)//返回NULL表示打开文件失败
{
printf("%s\n", strerror(errno));//失败,错误码保存在errno中,调用函数翻译打印失败的原因
//perror("fopen");//不需要传递errno,实际上它不仅将错误码转换了,而且还自动将其打印
return 1;
}
fclose(pf);
pf = NULL;
6.字符分类函数
1.iscntrl()任何控制字符 2.isspace()空白字符 3.isdigit()十进制数字0-9 4.isxdigit()十六进制数字
5.islower()小写字母 6.isupper()大写字母 7.isalpha()英文字母 8.isalnum()字母或数组
9.ispunct()标点符号,任何不属于数字或字母的图形字符(可打印) 10.isgraph()任何图形字符
11.isprint()任何可打印字符,包括图形字符和空白字符
char ch = 'i';
printf("%d\n",isdigit(ch));//非数字字符返回0;否则返回非0
printf("%d\n",isalpha(ch));//非英文字母字符返回0;否则返回非0
7.字符转换函数
(1)tolower()转换为小写字母,返回ASCLL码值
(2)toupper()转换为大写字母,返回ASCLL码值
char arr[10] = {0};
scanf("%s", arr);
int i = 0;
while ('\0' != arr[i])
{
if (isupper(arr[i]));
{
arr[i] = tolower(arr[i]);
}
printf("%c ", arr[i]);
i++;
}
int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//01 00 00 00 02 00 00 00
int arr2[10] = {0};
memcpy(arr2,arr1,20);
模拟实现memcpy(),见「地表最强」C语言(十六)一些自定义函数和宏16.1.1
//将12345拷贝到34567
int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
memmove(arr1 + 2, arr1, 20);
//若使用memcpy
int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
memcpy(arr1 + 2, arr1, 20);
虽然结果一样,但是两者的应用场景应该区分开来:memcpy()函数是为了不重叠的拷贝而设计的,虽然有的编译器上这个函数也支持重叠的拷贝,但并不是所有的都支持;而memmove()就是为了重叠拷贝而设计的。
模拟实现memmove(),见「地表最强」C语言(十六)一些自定义函数和宏16.1.7
float arr1[] = {1.0,2.0,3.0,4.0};
float arr2[] = {1.0,3.0,3.0};
printf("%d\n",memcmp(arr1,arr2,8));//相等返回0,小于返回负数,大于返回正数
int arr[10] = {0};
memset(arr,1,20);//将arr的前20个字节设置为1