第 15 节:C语言常用函数

🔢 C语言常用函数

本次笔记参考哔站尚硅谷宋红康老师的C语言教程。C语言是一种广泛应用的编程语言,它提供了一系列的标准库函数,使得程序员能够更高效地编写程序。函数是C语言编程中的基础,通过它们,程序员可以构建出功能丰富的应用程序。掌握这些函数的使用方法对于C语言程序员来说是非常重要。

📌 字符串相关函数

1.1 字符串的表示方式

C 语言没有单独的字符串类型,字符串被当作字符数组,即 char 类型的数组。表示方式如下:

方式1:

C 复制代码
char str[] = "hello";

方式2:

C 复制代码
char *str = "hello";

1.2 两种方式的区别

字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是也有区别。

区别1: 指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。

C 复制代码
char *str = "hello!";
str[0] = 'z'; // 报错

如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。

C 复制代码
char str[] = "hello";
str[0] = 'z'; // 不报错

区别2: 指针变量可以指向其它字符串。

C 复制代码
char *s = "hello";
s = "world";

但是,字符数组变量不能指向另一个字符串。

C 复制代码
char s[] = "hello";
s = "world"; // 报错

1.3 基本数据类型和字符串的转换

在程序开发中,我们经常需要将基本数据类型转成字符串类型(即 char 数组 )。或者将字符串类型转成基本数据类型。

基本数据类型 -> 字符串

sprintf()函数可以将其他数据类型转换成字符串类型。此函数声明在stdio.h头文件中。

C 复制代码
#include <stdio.h>

int main() {
    char str1[20]; // 字符数组,即字符串
    char str2[20];
    char str3[20];
    int a = 111, b = 222;
    char c = 'a';
    double d = 333.444;

    sprintf(str1, "%d %d", a, b);
    sprintf(str2, "%d%c", a, c);
    sprintf(str3, "%.5f", d);
    printf("str1=%s\n", str1); // 111 222
    printf("str2=%s\n", str2); // 111a
    printf("str3=%s\n", str3); // 333.44400

    return 0;
}

字符串 -> 基本数据类型

调用头文件stdlib.h 的函数atoi()atof() 即可。

C 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    char str1[10] = "123456";
    char str2[4] = "111";
    char str3[10] = "12.67423";
    char str4[2] = "a";

    int i = atoi(str1);
    int j = atof(str1);
    short s = atoi(str2);
    double d = atof(str3);
    char c = str4[0];
    printf("i=%d,j=%d,s=%d,d=%lf,c=%c", i, j, s, d, c);

    return 0;
}

📌 日期和时间相关函数

在编程中,程序员会经常使用到日期相关的函数,比如:统计某段代码执行花费的时间等等。头文件是 <time.h>

举例说明:

  • time_t time(time_t *t):返回一个值,即格林尼治时间1970年1月1日00:00:00到当前时刻的时长,时长单位是秒。
  • char *ctime(const time_t *timer):获取当前时间,返回一个表示当地时间的字符串(当地时间是基于参数timer的)。
  • double difftime(time_t time1, time_t time2):计算time1和time2之间相差的秒数(time1-time2)。

举例:

C 复制代码
#include <stdio.h>
#include <time.h> // 该头文件中,声明日期和时间相关的函数

void test() {
    int i = 0;
    int sum = 0;
    int j = 0;
    for (i = 0; i < 10000000; i++) {
        sum = 0;
        for (j = 0; j < 100; j++) {
            sum += j;
        }
    }
}

int main() {
    printf("程序启动...\n");

    time_t start_t;
    // 先得到执行test前的时间
    time(&start_t); // 获取当前时间

    test(); // 执行test

    time_t end_t;
    // 再得到执行test后的时间
    time(&end_t); // 获取当前时间

    double diff_t; // 存放时间差
    diff_t = difftime(end_t, start_t); // 时间差,按秒 ent_t - start_t

    // 然后得到两个时间差就是耗用的时间
    printf("%d\n", start_t); // 1697026306
    printf("%d\n", end_t); // 1697026308
    printf("执行test()函数 耗用了%.2f 秒\n", diff_t); // 执行test()函数 耗用了2.00 秒

    // 获取时间对应的字符串的表示
    char *startTimeStr = ctime(&start_t);
    printf("%s\n", startTimeStr); // Wed Oct 11 20:11:48 2023

    return 0;
}

📌 数学运算相关的函数

math.h头文件定义了各种数学函数。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。

  • double exp(double x):返回 e 的 x 次幂的值。
  • double log(double x):返回 x 的自然对数(基数为 e 的对数)。
  • double pow(double x, double y):返回 x 的 y 次幂。
  • double sqrt(double x):返回 x 的平方根。
  • double fabs(double x):返回 x 的绝对值。
C 复制代码
#include <stdio.h>
#include <math.h>

int main() {
    double d1 = pow(2.0, 3.0);
    double d2 = sqrt(5.0);

    printf("d1=%.2f\n", d1); // d1=8.00
    printf("d2=%f\n", d2); // d2=2.236068

    return 0;
}

📌 内存管理相关函数

4.1 C程序的内存分配

C程序中,不同数据在内存中分配说明:

  1. 全局变量和静态局部变量------内存中的静态存储区/全局区
  2. 非静态的局部变量------内存中的动态存储区:stack 栈
  3. 临时使用的数据------建立动态内存分配区域,需要时随时开辟,不需要时及时释放------heap 堆

4.2 void 指针(无类型指针)

  • 每一块内存都有地址,通过指针变量可以获取指定地址的内存块。
  • 指针变量必须有类型,否则编译器无法知道如何解读内存块保存的二进制数据。但是,向系统请求内存的时候,有时不确定会有什么样的数据写入内存,需要先获得内存块,稍后再确定写入的数据类型。

综上,为了满足这种需求,C 语言提供了一种不定类型的指针,叫做 void 指针。它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。

此外,由于void 指针等同于无类型指针(typeless pointer),可以指向任意类型的数据,但是不能解读数据。void 指针与其他所有类型指针之间是互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。

C 复制代码
int x = 10;
void *p = &x; // 整数指针转为 void 指针
int *q = p; // void 指针转为整数指针

4.3 内存动态分配函数

头文件<stdlib.h>声明了四个关于内存动态分配的函数。所谓动态分配内存,就是按需分配,申请才能获得。

掌握:malloc()

函数原型:

C 复制代码
void *malloc(unsigned int size); // size的类型为无符号整型

作用:在内存的动态存储区(堆区)中分配一个长度为size的连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。由于返回的指针的基类型为void,应通过显式类型转换后才能存入其他基类型的指针变量,否则会有警告。如果分配不成功,返回空指针(NULL)。

举例1:

C 复制代码
int *p;
p = (int *)malloc(sizeof(int));

举例2:动态申请数组空间

C 复制代码
int *p;
p = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
    p[i] = i * 5;

得到一个元素类型为int型,长度为n的数组。取元素方式与之前相同,如获取第2个元素:p[1]。

举例3:

C 复制代码
struct node *p;
p = (struct node *)malloc(sizeof(struct node)); // (struct node*)为强制类型转换
C 复制代码
typedef struct BTNode {
    int data;
    struct BTNode *lchild;
    struct BTNode *rchild;
} BTNode;

//声明二叉树结点方式1
BTNode bt1;
//声明二叉树结点方式2:需熟练掌握
BTNode *bt2;
bt2 = (BTNode *)malloc(sizeof(BTNode));

考研数据结构中所有类型结点的内存分配都可以用函数malloc()来完成,模式固定,容易记忆。

方式2中的BT是指针型变量,还可以指向其它节点。而方式1中的BT则不行。此外,调用结构体成员时,

C 复制代码
//针对于方式1:结构体变量取成员,用"."
int x = bt1.data;

//针对于方式2:指向结构体的指针取成员,用"->"
int x = bt2->data;
int x = (*bt2).data; //以前的写法

关于返回值为NULL:

malloc() 分配内存有可能分配失败,这时返回常量 NULL。Null 的值为0,是一个无法读写的内存地址,可以理解成一个不指向任何地方的指针。它在包括 stdlib.h 等多个头文件里面都有定义,所以只要可以使用 malloc() ,就可以使用 NULL 。由于存在分配失败的可能,所以最好在使用 malloc() 之后检查一下,是否分配成功。

C 复制代码
int *p = (int *)malloc(sizeof(int));
if (p == NULL) { // 内存分配失败

}
// 或
if (p != NULL) {
    //...
}

上面示例中,通过判断返回的指针 p 是否为 NULL ,确定 malloc() 是否分配成功。

了解:calloc()

函数原型:

C 复制代码
void *calloc(unsigned int n, unsigned int size);

作用:在内存的动态存储区(堆区)中分配n个,单位长度为size的连续空间,这个空间一般比较大,总共占用n*size 个字节。并将该空间的首地址作为函数的返回值。如果函数没有成功执行,返回NULL。

calloc()函数适合为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。

举例:

C 复制代码
int *p;
p = (int *)calloc(10, sizeof(int)); // 开辟空间的同时,其内容初始化为零

// 等同于
int *p;
p = (int *)malloc(10 * sizeof(int));
memset(p, 0, sizeof(int) * 10);

上面示例中, calloc() 相当于 malloc() + memset() 。

了解:realloc()

函数原型:

C 复制代码
void *realloc(void *p, unsigned int size)

作用:重新分配malloc()或calloc()函数获得的动态空间大小,即调整大小的内存空间。将先前开辟的内存块的指针p指向的动态空间大小改变为size,单位字节。返回值是一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。分配失败返回NULL。

  • realloc() 优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。
  • 如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用 memset() )。

举例1:

C 复制代码
int *b;
b = (int *)malloc(sizeof(int) * 10);
b = (int *)realloc(b, sizeof(int) * 2000);

指针 b 原来指向10个成员的整数数组,使用 realloc() 调整为2000个成员的数组。

举例2:动态栈入栈时,判断是否需要扩容

C 复制代码
int push(SqStack &S, ElemType e) {
    if (S.top - S.bottom >= S.stacksize) { // 栈满,追加存储空间
        S.bottom = (ElemType *)realloc(S.bottom, (STACKINCREMENT + S.stacksize) * sizeof(ElemType));
        if (!S.bottom) // if(S.bottom == NULL)
            return FALSE; // 空间分配失败
        S.top = S.bottom + S.stacksize;
        S.stacksize += STACKINCREMENT;
    }
    *S.top = e;
    S.top++; // 栈顶指针加1
    return TRUE;
}

掌握:free()

函数原型:

C 复制代码
void free(void *p);

函数无返回值。p是最近一次调用malloc()或calloc()函数时的返回值。

作用:释放指针变量p所指向的内存空间,使这部分内存能重新被其它变量使用。否则这个内存块会一直占用到程序运行结束。

举例:

C 复制代码
int *p;
p = (int *)malloc(sizeof(int));
// ...各种操作...
free(p); // 千万不要忘了使用free()释放内存!

注意:

  1. 指针 p 必须是经过动态分配函数 malloc() 成功后返回的首地址。

  2. 分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。

  3. 如果忘记调用free()函数,同时p所在的函数调用结束后p指针已经消失了,导致无法访问未回收的内存块,构成内存泄漏。

4.4 举例

举例:动态创建数组,输入5个学生的成绩,另外一个函数检测成绩低于60 分的,输出不合格的成绩。

C 复制代码
#include <stdio.h>
#include <stdlib.h>

#define N 5

void check(int *ptr) {
    printf("\n不及格的成绩有: ");
    for (int i = 0; i < 5; i++) {
        if (ptr[i] < 60) {
            printf(" %d ", ptr[i]);
        }
    }
}

int main() {
    int *p;
    // 动态创建数组
    p = (int *)malloc(N * sizeof(int));

    printf("请输入%d个成绩:\n", N);
    for (int i = 0; i < N; i++) {
        scanf("%d", p + i);
    }
    // 检查不及格的学生
    check(p);

    free(p); // 销毁 堆区 p 指向的空间

    return 0;
}

4.5 动态分配内存的基本原则

1)避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大。 2)仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它,否则可能出现内存泄漏。 3)总是确保释放以分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存。

4.6 常见的内存错误及其对策

1)内存分配未成功,却使用了它

新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。比如,如果指针p是函数的参数,那么在函数的入口处应该用if(p==NULL)if(p!=NULL)进行防错处理。

2)内存分配虽然成功,但是尚未初始化就引用它

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误。

C 复制代码
int * p = NULL;
p = (int *)malloc(sizeof(int));
if (p == NULL) { /*...*/ }
/*初始化为0*/
memset(p, 0

, sizeof(int));

c 复制代码
题外话,无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

**3)内存分配成功并且已经初始化,但操作时提示内存越界**

在使用数组时经常发生下标"+1"或者"-1"的操作,特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

数组访问越界在运行时,它的表现是不定的,有时什么事也没有,程序一直运行(当然,某些错误结果已造成);有时,则是程序一下子崩溃。

**4)忘记了释放内存,造成内存泄漏**

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

动态内存的申请与释放必须配对,程序中`malloc()`与`free()`的使用次数一定要相同,否则肯定有错误。

**5)未正确的释放内存,造成内存泄漏**

程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存。此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

```C
#include <stdio.h>
#include <stdlib.h>

void getMemory(int *p) {
    p = (int *)malloc(sizeof(int)); // 在这里修改的是局部指针 p,不会影响 main 函数中的原始指针 ptr
    //...
}

int main() {
    int *ptr = NULL;
    getMemory(ptr); // 将 ptr 的值传递给 getMemory,但是在函数内部修改的是 p,而不是 ptr
    printf("ptr = %d\n", *ptr); // 这里的 *ptr 是未定义行为,因为 ptr 没有指向有效的内存
    free(ptr); // 这里试图释放未分配的内存,会导致问题
}

在本例中,getMemory()中的p申请了新的内存,只是把 p所指的内存地址改变了,但是ptr丝毫未变。getMemory()中的p也始终没有进行内存的释放。事实上,因为没有用free释放内存,每执行一次getMemory()就会泄漏一块内存。

6)释放了内存却继续使用它

函数的return语句写错了,注意不要返回指向"栈内存"的"指针"或者"引用",因为该内存在函数体结束时被自动销毁。

C 复制代码
long *p;

void addr() {
    long k;
    k = 0;
    p = &k;
}
void port() {
    long i, j;
    j = 0;
    for (i = 0; i < 10; i++) {
        (*p)--;
        j++;
    }
}


int main() {
    addr();
    port();
}

由于addr函数中的变量k在函数返回后就已经不存在了,但是在全局变量p中却保存了它的地址。在下一个函数port中,试图通过全局指针p访问一个不存在的变量,进而出错。

在计算机系统,特别是嵌入式系统中,内存资源是非常有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何有效地管理内存资源。 || 我是稀土掘金的一名博主,希望你优化布局并添加emoji表情。

相关推荐
愿天垂怜1 分钟前
【C++】C++11引入的新特性(1)
java·c语言·数据结构·c++·算法·rust·哈希算法
mit6.8246 分钟前
[Redis#4] string | 常用命令 | + mysql use:cache | session
数据库·redis·后端·缓存
大帅哥_8 分钟前
访问限定符
c语言·c++
小林熬夜学编程38 分钟前
【Linux系统编程】第五十弹---构建高效单例模式线程池、详解线程安全与可重入性、解析死锁与避免策略,以及STL与智能指针的线程安全性探究
linux·运维·服务器·c语言·c++·安全·单例模式
我qq不是451516521 小时前
C语言指针作业
c语言
苏言の狗1 小时前
小R的二叉树探险 | 模拟
c语言·数据结构·算法·宽度优先
加载中loading...1 小时前
C/C++实现tcp客户端和服务端的实现(从零开始写自己的高性能服务器)
linux·运维·服务器·c语言·网络
捂月1 小时前
Spring Boot 核心逻辑与工作原理详解
java·spring boot·后端
Nightselfhurt1 小时前
RPC学习
java·spring boot·后端·spring·rpc
Heisenberg~3 小时前
详解八大排序(五)------(计数排序,时间复杂度)
c语言·数据结构·排序算法