sizeof与strlen.md

本文深入解析C/C++中sizeof运算符与strlen函数的区别,通过代码示例阐述它们在处理数据类型大小与字符串长度上的不同作用。sizeof用于计算类型或变量所占内存大小,而strlen用于测量以''结尾的字符串的实际长度。

**sizeof与strlen的区别: **

下面以代码的形式来解释
#include <iostream>
using namespace std;
int main()
{
	cout << sizeof(int) <<"\t"<<sizeof(double)<< endl;
	
	int x = 3;
	cout << sizeof(x) << endl;

	//cout << strlen(4) << endl;	//error: strlen只能以\0结尾的参数作为参数
	cout << strlen("abc123d") << endl;	// 系统会自动加个'\0'作为结束符

	int arr[] = { 1,2,3 };
	cout << sizeof(arr) << endl;	// 数组作为sizeof参数时,不退化
	//cout << strlen(arr) << endl;	// error:数组作为strlen的参数时,退化为指针,指针是一种普通的变量,从访问上不异与其他变量。
	//它保存的数值是整型类型,和整型数据不同的是,它指向的是一段内存地址。所以不是以‘\0’结束的字符串,所以报错

	/*

	两者区别:
	sizeof是一个操作数,而strlen是库函数
	sizeof的参数可以是数据的类型也可以是变量,而strlen的参数只能是以'\0'为结尾的字符串
	编译器在编译时期就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来
	sizeof计算的是类型所占内存的大小,而strlen计算的是字符串实际的长度
	数组作sizeof的参数不退化,而传递给strlen作参数就退化为指针了

	*/

	return 0;
}
int create_upgrade_file(const char* output_path, const char* part_names[], const char* part_files[], int part_count) { // 变量声明 FILE* in_fp = NULL; FILE* out_fp = NULL; uint8_t* data = NULL; MTD_UP__HEAD head; size_t total_size = 0; size_t padding_size = 0; size_t block_size = 2048; char *tmpaddr = NULL; const char* ext = NULL; char curtype[16] = {0}; int filesize = 0; int tmpsize = 0; int curmethod = 0; unsigned char md5_digest[16] = {'\0'}; // 函数主体 out_fp = fopen(output_path, "wb"); if (!out_fp) return -1; for (int i = 0; i < part_count; i++) { // printf("i:%d\n",i); in_fp = fopen(part_files[i], "rb"); // type 判断 ext = strrchr(part_files[i], '.'); curmethod = INT64_MIN; if (ext != NULL) { ext++; // 跳过点符号 // 后缀名匹配判断 if (strcmp(ext, "elf") == 0 || strcmp(ext, "mbn") == 0 || strcmp(ext, "bin") == 0) { snprintf(curtype, sizeof(curtype), "MTD"); curmethod = 1; } else if (strcmp(ext, "ubi") == 0) { snprintf(curtype, sizeof(curtype), "UBI"); curmethod = 0; } else { // 未知后缀处理 snprintf(curtype, sizeof(curtype), "UNKNOW"); curmethod = -1; } } else { // 无后缀名处理 snprintf(curtype, sizeof(curtype), "NO_EXT"); curmethod = -2; } if (!in_fp) { fclose(out_fp); return -1; } fseek(in_fp, 0, SEEK_END); int filesize = ftell(in_fp); rewind(in_fp); padding_size = (block_size - (filesize % block_size)) % block_size; total_size = filesize + padding_size; data = malloc(total_size); if (!data) { fclose(in_fp); fclose(out_fp); return -1; } if (fread(data, 1, filesize, in_fp) != filesize) { free(data); fclose(in_fp); fclose(out_fp); return -1; } fclose(in_fp); in_fp = NULL; memset(data + filesize, 0xFF, padding_size); memset(&head, 0, sizeof(head)); strncpy(head.name, part_names[i], sizeof(head.name) - 1); head.size = total_size; strncpy(head.type, curtype, sizeof(head.type) - 1); head.UPmethod = curmethod; // memcpy(head.md5, md5, sizeof(head.md5)); printf("name:%s;\n size:%u;\n type:%s;\n UPmethod:%d;\n",head.name,head.size,head.type,head.UPmethod); tmpaddr = (char *)malloc(16 + total_size); memcpy(tmpaddr, md5Key, 16); memcpy(tmpaddr + 16, data, total_size); tmpsize = 16 + total_size; md5_make_digest(md5_digest, (unsigned char *)(tmpaddr), tmpsize); printf("MD5:\n"); for (int i = 0; i < 16; i++) { if(i == 15){printf("%0x- \n", md5_digest[i]);} // 输出十六进制 else{printf("%0x- ", md5_digest[i]);} } memcpy(head.md5, md5_digest, 16); if (fwrite(&head, sizeof(head), 1, out_fp) != 1) { free(data); fclose(out_fp); return -1; } if (fwrite(data, 1, total_size, out_fp) != total_size) { free(data); fclose(out_fp); return -1; } free(data); data = NULL; free(tmpaddr); tmpaddr = NULL; } fclose(out_fp); return 0; } 我说的是上述函数
11-18
<think> 我们正在处理一个函数create_upgrade_file,该函数用于创建一个升级文件,其中包含多个部分(每个部分由part_names和part_files指定)。在之前的对话中,我们已经讨论过内存泄漏问题,现在要求继续优化该函数,增加错误时的内存释放,防止内存泄露等问题。 当前函数存在的主要问题: 1. 在循环中,如果某次迭代失败,函数直接返回,但没有释放之前已经分配的资源(如data, tmpaddr)以及关闭文件句柄(如in_fp, out_fp)。 2. 在循环内部,如果发生错误,需要清理当前循环内分配的资源,同时还要清理之前循环中已经成功分配的资源(比如之前已经写入的数据)?注意,这个函数的设计是:循环处理每个部分,将每个部分的信息和内容写入输出文件。如果中间某部分失败,整个函数就应该失败,并且需要释放所有已经分配的资源(包括输出文件)。 但是,观察当前代码,在循环内部,如果打开输入文件失败,它会关闭输出文件并返回。但是,这会导致之前已经成功写入的部分残留在输出文件中,而且之前分配的资源(比如之前循环中分配的内存)并没有被释放。 因此,我们需要重构代码,确保在发生错误时,能够释放所有已经分配的资源,包括: - 关闭所有打开的文件(输入和输出) - 释放所有分配的内存(data, tmpaddr) 我们可以采用“goto error”的方式,在函数末尾统一释放资源。但是注意,在循环中,我们需要记录每一步分配的资源,以便在错误时释放。 重构思路: 1. 在函数开头初始化所有指针为NULL,文件指针为NULL,这样在错误处理时可以通过判断指针是否为NULL来决定是否需要释放。 2. 使用一个错误处理标签(error),在发生错误时跳转到该标签,统一释放资源。 3. 在循环中,每次迭代分配的资源(如data, tmpaddr)在本次迭代结束时会释放(因为每次迭代都重新分配),但是如果在循环中发生错误,我们需要释放当前迭代分配的资源,同时跳出循环,并跳转到错误处理部分(该部分还会关闭输出文件,并删除可能已经部分写入的输出文件?但原函数没有删除输出文件,我们可以考虑在错误处理时删除输出文件,以防止残留部分数据)。 然而,由于循环中每次迭代都会重新分配data和tmpaddr,并且每次迭代结束后都会释放,所以循环中当前迭代的分配不会影响之前迭代(因为之前迭代已经释放了)。但是,如果我们在第i次迭代(i>0)时出错,那么在第i次迭代中分配的资源(data, tmpaddr)需要释放,同时之前迭代已经写入输出文件的数据实际上已经成功写入,并且我们无法回滚(除非在错误处理时删除输出文件)。另外,输出文件在循环之前就已经打开,如果循环中发生错误,我们需要关闭输出文件,但原函数在第一次打开输入文件失败时关闭了输出文件并返回,但此时输出文件已经创建,可能包含部分数据。 因此,我们可能需要: - 在错误处理中,除了释放内存和关闭文件,还要删除可能已经部分写入的输出文件(使用remove函数)。 - 或者,我们可以先写入一个临时文件,等所有部分都成功写入后再重命名为目标文件(这样要么成功得到完整文件,要么失败没有文件)。 但是,原函数是直接打开输出文件(output_path)进行写入。这样在写入过程中出错,会留下一个不完整的文件。 因此,我们考虑改进: 1. 使用临时文件:先打开一个临时文件(比如在output_path后加一个后缀.tmp),所有内容写入临时文件。 2. 全部成功后再重命名临时文件为目标文件。 3. 如果发生错误,则删除临时文件。 这样,可以避免留下不完整的输出文件。 另外,我们还需要在循环中处理错误时,释放当前循环分配的资源,然后跳出循环,进入统一的错误处理(关闭所有文件,释放所有资源,删除临时文件)。 步骤: 1. 修改输出文件的打开方式:先创建一个临时文件(文件名可以是output_path加".tmp"后缀)。 2. 在循环中,如果遇到错误,使用goto跳转到错误处理标签。 3. 错误处理标签处:关闭输入文件(如果打开的话),释放当前循环分配的内存(data和tmpaddr),然后跳出循环?实际上,我们可以在错误处理中统一处理:因为每次循环中分配的内存(data和tmpaddr)都是在循环内部分配的,并且每次循环结束后都会释放,所以如果发生错误,我们只需要释放当前循环中分配的内存(因为之前循环的已经释放了),然后关闭当前循环中打开的输入文件(in_fp),然后跳转到错误处理(关闭输出文件,删除临时文件,释放其他资源?注意,这里没有其他资源了,除了临时文件路径字符串)。 但是,我们还需要注意:在循环内部,我们可能已经为当前部分分配了data和tmpaddr,并且在错误发生前,这些资源需要释放。所以,我们在循环内部发生错误时,应该先释放当前分配的资源,然后跳转到统一的错误处理(这个错误处理也会处理输出文件和临时文件)。 然而,为了统一管理,我们可以将循环内部分配的资源(data和tmpaddr)的释放放在循环内部,然后使用一个状态码来标记错误,然后跳出循环,再在循环外进行错误处理(关闭输出文件,删除临时文件等)。 但是,使用goto可以更清晰地在同一个地方处理错误。 我们重新设计: 变量声明: FILE* in_fp = NULL; FILE* out_fp = NULL; uint8_t* data = NULL; ... char *tmpaddr = NULL; ... char* tmp_output_path = NULL; // 临时文件路径 步骤: 1. 构建临时文件路径:tmp_output_path = malloc(strlen(output_path)+5); sprintf(tmp_output_path, "%s.tmp", output_path); 2. 打开临时文件:out_fp = fopen(tmp_output_path, "wb"); 3. 循环处理每个部分: in_fp = fopen(part_files[i], "rb"); if (!in_fp) goto error; // 跳转到错误处理 ... 其他操作,如果出错则goto error; // 每次循环结束前,释放本次循环分配的内存(data和tmpaddr)并关闭输入文件 free(data); data = NULL; free(tmpaddr); tmpaddr = NULL; fclose(in_fp); in_fp = NULL; 4. 循环结束后,关闭输出文件,然后将临时文件重命名为目标文件,释放临时文件路径内存,返回0。 5. 错误处理: error: // 先释放当前循环中可能分配的资源(因为错误可能发生在循环中间) if (data) { free(data); data=NULL; } if (tmpaddr) { free(tmpaddr); tmpaddr=NULL; } if (in_fp) { fclose(in_fp); in_fp=NULL; } // 然后,关闭输出文件(如果打开) if (out_fp) { fclose(out_fp); out_fp=NULL; } // 删除临时文件 if (tmp_output_path) { remove(tmp_output_path); free(tmp_output_path); } return -1; 但是,上述错误处理在循环内发生错误时,会跳出循环,然后执行错误处理。但是,在循环内部,每次迭代我们都会在最后释放data和tmpaddr,所以当发生错误时,我们还没有释放本次迭代分配的data和tmpaddr(因为错误发生在释放之前),所以在错误处理中需要释放它们。同时,输入文件(in_fp)也需要关闭。 但是,注意:在循环中,如果某次迭代失败,我们跳转到error,然后会释放当前data和tmpaddr以及关闭in_fp,然后关闭out_fp,删除临时文件,返回错误。 然而,这样处理有一个问题:在循环中,如果第一次迭代成功,第二次迭代失败,那么第一次迭代写入的数据已经在临时文件中,但整个操作失败,临时文件会被删除,这是符合预期的。 但是,还有一个问题:在循环中,我们每次都会重新分配data和tmpaddr,所以每次迭代的data和tmpaddr都是独立的,不会累积。但是,在错误处理中,我们只释放了当前迭代的data和tmpaddr(因为每次迭代开始时会重新赋值,所以发生错误时,data和tmpaddr指向的是当前迭代分配的内存),而之前迭代的data和tmpaddr在之前迭代的末尾已经被释放了。 所以,这个方案是可行的。 但是,注意:在循环内部,如果某次迭代成功,我们在迭代末尾释放了data和tmpaddr,并将它们置为NULL,这样在错误处理中再次释放(因为错误处理中会判断是否为NULL)也不会造成问题。 另外,临时文件路径的分配:我们使用malloc分配,所以在错误处理中需要释放。 因此,我们重构代码: 注意:原函数中,在循环内部,每次迭代都会分配data和tmpaddr,并且在迭代结束前释放(除了最后一次)。所以,我们在重构时,循环内部的结构保持不变,只是在发生错误时,跳转到error标签,并在error标签中处理当前迭代的资源以及临时文件。 但是,在循环中,我们还需要注意:在循环内部,如果发生错误,我们跳转到error,那么循环将不会继续,错误处理会立即执行。 代码结构如下: ```c int create_upgrade_file(const char* output_path, const char* part_names[], const char* part_files[], int part_count) { // 变量声明 FILE* in_fp = NULL; FILE* out_fmp = NULL; // 我们将输出文件指针重命名为out_fmp(避免拼写错误混淆) uint8_t* data = NULL; MTD_UP__HEAD head; size_t total_size = 0; size_t padding_size = 0; size_t block_size = 2048; char *tmpaddr = NULL; const char* ext = NULL; char curtype[16] = {0}; int filesize = 0; int tmpsize = 0; int curmethod = 0; unsigned char md5_digest[16] = {'\0'}; // 新增:临时文件路径 char* tmp_output_path = NULL; // 创建临时文件路径 tmp_output_path = malloc(strlen(output_path) + 5); if (!tmp_output_path) { return -1; } snprintf(tmp_output_path, strlen(output_path)+5, "%s.tmp", output_path); // 打开临时输出文件 out_fmp = fopen(tmp_output_path, "wb"); if (!out_fmp) { free(tmp_output_path); return -1; } for (int i = 0; i < part_count; i++) { // 打开当前部分的文件 in_fp = fopen(part_files[i], "rb"); if (!in_fp) { goto error; } // 判断文件类型 ext = strrchr(part_files[i], '.'); curmethod = INT_MIN; // 使用一个特殊值表示未初始化 if (ext != NULL) { ext++; if (strcmp(ext, "elf") == 0 || strcmp(ext, "mbn") == 0 || strcmp(ext, "bin") == 0) { snprintf(curtype, sizeof(curtype), "MTD"); curmethod = 1; } else if (strcmp(ext, "ubi") == 0) { snprintf(curtype, sizeof(curtype), "UBI"); curmethod = 0; } else { snprintf(curtype, sizeof(curtype), "UNKNOW"); curmethod = -1; } } else { snprintf(curtype, sizeof(curtype), "NO_EXT"); curmethod = -2; } // 获取文件大小 if (fseek(in_fp, 0, SEEK_END) != 0) { goto error; } filesize = ftell(in_fp); if (filesize < 0) { goto error; } rewind(in_fp); // 计算填充大小 padding_size = (block_size - (filesize % block_size)) % block_size; total_size = filesize + padding_size; // 分配数据缓冲区 data = malloc(total_size); if (!data) { goto error; } // 读取文件内容 if (fread(data, 1, filesize, in_fp) != filesize) { goto error; } fclose(in_fp); in_fp = NULL; // 填充0xFF memset(data + filesize, 0xFF, padding_size); // 初始化头部 memset(&head, 0, sizeof(head)); strncpy(head.name, part_names[i], sizeof(head.name) - 1); head.name[sizeof(head.name)-1] = '\0'; // 确保字符串结束 head.size = total_size; strncpy(head.type, curtype, sizeof(head.type) - 1); head.type[sizeof(head.type)-1] = '\0'; head.UPmethod = curmethod; // 计算MD5 tmpaddr = malloc(16 + total_size); if (!tmpaddr) { goto error; } memcpy(tmpaddr, md5Key, 16); // 假设md5Key是全局变量,这里直接使用 memcpy(tmpaddr + 16, data, total_size); tmpsize = 16 + total_size; md5_make_digest(md5_digest, (unsigned char *)tmpaddr, tmpsize); memcpy(head.md5, md5_digest, 16); // 写头部 if (fwrite(&head, sizeof(head), 1, out_fmp) != 1) { goto error; } // 写数据 if (fwrite(data, 1, total_size, out_fmp) != total_size) { goto error; } // 释放当前部分的内存 free(data); data = NULL; free(tmpaddr); tmpaddr = NULL; } // end for // 成功完成所有部分,关闭输出文件 fclose(out_fmp); out_fmp = NULL; // 重命名临时文件为目标文件 if (rename(tmp_output_path, output_path) != 0) { // 重命名失败,删除临时文件 remove(tmp_output_path); free(tmp_output_path); return -1; } free(tmp_output_path); return 0; error: // 释放当前循环分配的资源 if (data) { free(data); data = NULL; } if (tmpaddr) { free(tmpaddr); tmpaddr = NULL; } if (in_fp) { fclose(in_fp); in_fp = NULL; } // 关闭输出文件 if (out_fmp) { fclose(out_fmp); out_fmp = NULL; } // 删除临时文件 if (tmp_output_path) { remove(tmp_output_path); free(tmp_output_path); tmp_output_path = NULL; } return -1; } ``` 注意: - 在循环中,每次迭代开始,in_fp、data、tmpaddr都被重新赋值(前一次迭代的已经被释放和置空)。 - 在错误处理中,我们释放当前迭代可能分配的资源(data, tmpaddr)和关闭当前迭代的输入文件(in_fp)。 - 输出文件(out_fmp)在错误处理中被关闭,然后删除临时文件。 - 在循环正常结束后,我们关闭输出文件,然后重命名临时文件为目标文件。如果重命名失败,我们删除临时文件并返回错误。 - 注意:在重命名之前,输出文件已经关闭(fclose),所以重命名是安全的。 另外,原函数中在计算文件大小时使用了ftell,它返回的是long,而filesize被声明为int,这可能在文件大于2GB时出现问题。但是,考虑到升级文件通常不会太大,这里暂时不做修改。 此外,在计算MD5时,原函数使用了一个全局变量md5Key(16字节),这里假设它已经定义。 这个优化版本确保了在发生错误时,所有已分配的内存和打开的文件都会被正确释放/关闭,并且不会留下临时文件(成功时会重命名,失败时会删除临时文件)。 注意:在错误处理中,我们并没有区分错误发生的位置,因为无论哪里出错,我们都需要执行相同的清理操作。 但是,注意:在循环中,如果fclose(in_fp)成功执行,我们会将in_fp置为NULL,这样在错误处理中就不会再次关闭(避免重复关闭)。其他资源同理。 这个实现符合要求吗?是的,它解决了内存泄漏和资源释放问题,并确保文件系统不会残留临时文件。 但是,请注意:在循环中,每次迭代都会重新分配data和tmpaddr,并且每次迭代结束都会释放,所以内存不会随着循环累积。 另外,在错误处理中,我们只处理当前迭代分配的资源,而之前迭代的资源在之前迭代的末尾已经被释放,所以不会造成泄漏。 最后,我们使用临时文件来确保输出文件的原子性:要么整个文件写入成功,要么没有任何输出文件(或者只有之前的完整文件,但不会有一个部分写入的文件)。 因此,我们完成了函数的优化。 注意:在重命名之前,确保输出文件已经关闭(使用fclose),否则在某些系统上重命名可能会失败。 另外,错误处理中的remove函数可能会失败(比如文件不存在),但我们不关心,因为我们的目的是清理。 以上。 现在,根据要求,在最后提出3-5个相关问题。 </think> ### 优化后的 `create_upgrade_file` 函数(内存安全版) ```c int create_upgrade_file(const char* output_path, const char* part_names[], const char* part_files[], int part_count) { // 变量声明(全部初始化为安全状态) FILE* in_fp = NULL; FILE* out_fp = NULL; uint8_t* data = NULL; char* tmpaddr = NULL; char* tmp_output_path = NULL; int ret = -1; // 默认失败 // 创建临时文件路径(原子性操作) size_t tmp_len = strlen(output_path) + 8; if (!(tmp_output_path = malloc(tmp_len))) goto cleanup; snprintf(tmp_output_path, tmp_len, "%s.tmp", output_path); // 打开临时输出文件 if (!(out_fp = fopen(tmp_output_path, "wb"))) goto cleanup; // 处理每个分区 for (int i = 0; i < part_count; i++) { // 打开输入文件 if (!(in_fp = fopen(part_files[i], "rb"))) goto per_part_error; /* 文件类型检测逻辑(保持不变)*/ /* ... 原有类型检测代码 ... */ // 获取文件大小 if (fseek(in_fp, 0, SEEK_END) != 0) goto per_part_error; long filesize = ftell(in_fp); if (filesize < 0) goto per_part_error; rewind(in_fp); // 计算对齐大小 const size_t block_size = 2048; size_t padding_size = (block_size - (filesize % block_size)) % block_size; size_t total_size = filesize + padding_size; // 分配数据缓冲区 if (!(data = malloc(total_size))) goto per_part_error; // 读取文件内容 if (fread(data, 1, filesize, in_fp) != (size_t)filesize) goto per_part_error; // 关闭输入文件(及时释放资源) fclose(in_fp); in_fp = NULL; // 填充对齐数据 memset(data + filesize, 0xFF, padding_size); /* 头部信息处理(保持不变)*/ /* ... 原有头部处理代码 ... */ // 分配MD5计算缓冲区 const size_t md5_buf_size = 16 + total_size; if (!(tmpaddr = malloc(md5_buf_size))) goto per_part_error; /* MD5计算逻辑(保持不变)*/ /* ... 原有MD5计算代码 ... */ // 写入文件头 if (fwrite(&head, sizeof(head), 1, out_fp) != 1) goto per_part_error; // 写入分区数据 if (fwrite(data, 1, total_size, out_fp) != total_size) goto per_part_error; // 释放当前分区资源 free(data); data = NULL; free(tmpaddr); tmpaddr = NULL; continue; // 继续下一分区 per_part_error: // 当前分区错误处理 if (in_fp) fclose(in_fp); if (data) free(data); if (tmpaddr) free(tmpaddr); in_fp = NULL; data = NULL; tmpaddr = NULL; goto cleanup; // 终止整个流程 } // 成功完成所有分区 ret = 0; cleanup: // 统一资源清理 // 关闭输出文件 if (out_fp) { fclose(out_fp); // 根据操作结果处理临时文件 if (ret == 0) { rename(tmp_output_path, output_path); // 原子替换 } else { remove(tmp_output_path); // 删除无效文件 } } // 释放内存资源 if (tmp_output_path) free(tmp_output_path); if (data) free(data); if (tmpaddr) free(tmpaddr); if (in_fp) fclose(in_fp); return ret; } ``` ### 关键优化点说明 1. **原子文件操作** ```c // 创建临时文件 snprintf(tmp_output_path, tmp_len, "%s.tmp", output_path); // 成功时原子替换 if (ret == 0) rename(tmp_output_path, output_path); ``` - 避免产生不完整的输出文件 - 失败时自动清理临时文件 2. **分层错误处理** ```c per_part_error: // 分区级错误 if (in_fp) fclose(in_fp); if (data) free(data); goto cleanup; // 跳转到全局清理 cleanup: // 全局资源清理 if (out_fp) fclose(out_fp); if (tmp_output_path) free(tmp_output_path); ``` - 分区级错误立即释放当前资源 - 全局清理确保无资源泄漏 3. **安全资源释放** ```c // 释放前重置指针 free(data); data = NULL; free(tmpaddr); tmpaddr = NULL; ``` - 避免重复释放 - 明确资源生命周期 4. **文件操作强化** ```c // 文件大小获取增加错误检查 if (fseek(in_fp, 0, SEEK_END) != 0) goto error; long filesize = ftell(in_fp); if (filesize < 0) goto error; ``` - 所有文件操作添加返回值检查 - 使用`long`确保大文件兼容性 5. **内存安全实践** ```c // 分配立即检查 if (!(data = malloc(total_size))) goto error; // 使用后立即释放 free(data); data = NULL; ``` - 每次`malloc`后必有对应的`free` - 指针使用后立即置空
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值