一直零零散散记录C语言知识点,为了之后更好查找将所学习的知识点进行了一个整合,方便自己和学回顾C语言的朋友,如有纰漏,欢迎指出。
#include < >
会在系统目录中查找头文件,而#include “ ”
先在当前项目目录之中查找,然后再去系统目录中查找,这样的做法也是防止头文件被重复包含。所以一般来说,如果使用的包含标准库头文件,就使用 <>,包含着自己的头文件的话就使用 “ ” 。 类型 无符号(unsigned) 有符号(signed)
char 0~+255 -128~+127
short 0~65535 -32768~32768
int 0~4294967295 -2147483648~+2147483648(21亿)
例如:char
的取值为0-255 则如果是它的数组,所能够容纳的值就只有256个
操作数类型不同,且不属于基本数据类型时,需要将操作数转化为所需类型,这个过程为强制类型转换,强制类型转换具有两种形式:显式强制转换和隐式强制类型转换。
显式强制类型转换:通过一定语句和相关的前缀,将所给出的类型强制转换成为我们所需要的类型。
int n=0xab65;
char a=(char)n;
强制类型转换的结果是将整型值0xab65的高端一个字节删掉,将低端一个字节的内容作为char型数值赋值给变量a,经过类型转换后n值并未改变。
隐式强制类型转换:当算术运算符两侧的操作数类型不匹配的时候,会触发“隐式的转换”,先转换成相同的类型,之后再进行计算。
C语言在以下四种情况下会进行隐式转换:
1、算术运算式中,低类型能转为高类型。
2、赋值表达式中,右边表达式的值自动隐式转换为左边变量的类型,并赋值给他。
3、函数调用中参数传递时,系统隐式地将实参转换为形参的类型后,赋给形参。
4、函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数。
在C语言中,自动类型转换遵循以下规则:
1、按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int量转成long型后再进行运算。
a、若两种类型的字节数不同,转换成字节数高的类型
b、若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型
2、所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。
4、char型和short型(在visual c++等环境下)参与运算时,必须先转换成int型。
5、在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度比左边长时,将丢失一部分数据,这样会降低精度,丢失的部分直接舍去。
一些需要注意的东西:
小技巧:想要实现运算之后的四舍五入计算,只需要给一个数加上0.5就可以直接完成。
例题1. unsigned int
其中unsigned int 不能表示小于0的,所以是恒大于0,则是永远成立的
unsigned int i;
for (i = 9; i >= 0; i--) {
printf("%u\n", i);// %u无符号十进制表示
}
因此此程序是一个无限循环的程序
例题2 .打印一个无符号的整型
char a=-128;
printf("%u",a);
unsigned int i;
i-1 => i+(-1)
把四个字节的变量赋值给一个字节的变量,会存在截断,则对应的截取它低位上的值:
四字节变量
int 0x11111111 11111111 11111111 11111111;
截断后一字节变量
char 0x1111 1111;
整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU是难以直接实现两个8比特字节的直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如:
char a,b,c;
a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中
整形提升是按照变量的数据类型的符号位来提升的:
/负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
/正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
/无符号整形提升,高位直接补0
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节.
表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof( c ) ,就是1个字节.
转义字符 | 释义 |
---|---|
\? | 在书中写连续多个问号时使用,防止他们被解析成为三个字母词 |
\’ | 用于表示字符串常量 ‘ |
\’’ | 用于表示一个字符串内部的双引号 |
\\ | 用于表示一个反斜杠,防止它被解释为一个转义序列符 |
\a | 警告字符,蜂鸣 |
\b | 退格符 |
\f | 进纸符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ddd | ddd表示1~3个八进制的数字。 如: \130 |
\xddd | ddd表示3个十六进制数字。 如: \x030 |
<< 左移操作符
>> 右移操作符
左移操作符移位规则:左边抛弃,右边补0
右移操作符移位规则:
首先右移运算分两种: -1
1. 逻辑移位 左边用0填充,右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丢弃
逻辑右移:左边补0;
算术右移:左边用原该值的符号位进行填充,由于是负数,所以符号位为1,即左边补1
注意:整数/0 所得到的结果显示为“运行时出错”
typedef 顾名思义是类型定义,这里应该理解为类型重命名。
//将unsigned int 重命名为uint_32, 所以uint_32也是一个类型名
typedef unsigned int uint_32;
int main()
{
//观察num1和num2,这两个变量的类型是一样的
unsigned int num1 = 0;
uint_32 num2 = 0;
return 0;
}
在C语言中:static是用来修饰变量和函数的
结论:static修饰局部变量改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。
结论:一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
结论:一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。
#define DEBUG_PRINT printf("file:%s line:%d", __FILE__, __LINE__)
#define For for(;;)
//1. 系统已经定义好的
printf("file:%s line:%d", __FILE__, __LINE__);
DEBUG_PRINT;
宏续接
1.
#define PRINT(FORMAT,VALUE) \
printf("the value of"#VALUE"is "FORMAT"\n",VALUE);
宏只会替换一行之中的后面部分,如果要写成多行,必须使用续行符 “\”
其中#则会把后面的字符换成了字符串
#define ADD_TO_SUM(num,value) \
sum##num += (value*3.14+10.1456);
其中##是把两边的符号合并成一个符号
double sum1 = 1, sum2 = 2,sum3 = 3;
ADD_TO_SUM(1, 10);
ADD_TO_SUM(2, 10);
ADD_TO_SUM(3, 10); // 则成为sum3+=(10*3.14+10.1456)
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
int bin_search(int arr[], int left, int right, int key)
{
int mid = 0;
while(left<=right)
{
mid = (left+right)>>1;
if(arr[mid]>key)
{
right = mid-1;
}
else if(arr[mid] < key)
{
left = mid+1;
}
else
return mid;//找到了,返回下标
}
return -1;//找不到
函数的参数分为实参和形参。
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
形参和实参的比较
通过函数的调用,函数之中拥有了和实参一模一样的内容,所以我们可以简单的认为:实参实例化之后其实就相当于实参的一份临时拷贝。
函数的声明
递归所产生的栈溢出
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者说是死递归的话,有可能导致一直在不断的开辟栈空间,最终导致栈空间耗尽,我们将这样的现象称为栈溢出。
如何解决递归溢出?
static
对象替代nonstatic
局部对象,在递归函数的设计中,可以使用static
对象替代nonstatic
局部对象(即所谓的栈对象),这样不仅可以减少每次递归调用和返回时所产生和释放的nonstatic
对象的开销,而且static
对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。数组是以组相同类型元素所构成的集合。
往往我们在写代码的时候,会将数组作为参数传个函数
#include
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
bubble_sort(arr, sz); //使用的时候,传数组元素个数
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
}
数组作为函数参数的时候,是不会把整个数组传递过去的,实际上只是把数组的首元素的地址传递过去了,所以即使在函数参数部分写成数组的形式:int arr[ ]
表示,依然会隐式转为一个指针:int * arr
。
柔性数组:结构体的最后可以定义一个大小是未知的这样一个数组。
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
如果使用sizeof(结构体)
的话,其中大小的计算不包含柔性数组。
结构是一些值的集合,这些值被我们称为成员变量,结构的每个成员可以是不同类型得变量。
例如我们想要去描述一个学生得具体信息:
typedef struct Stu
{
char name[20];名字
int age;年龄
char sex[5];性别
char id[20];学号
}Stu;分号不能丢
typedef struct Student
{
char name[128];
int age;
char tel[20];
}S; //S是结构体类型
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
一个结构体不能够包含自己类型的结构体变量,但可以包含自己类型的结构体指针。
struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
结构体的指针,该如何访问成员:
void print(struct Stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
小技巧: 结构体传参的时候,所传的一定要是结构体的地址,或者说是尽量去传指针!
#pragma pack(1)
则表示没有对齐数 括号里的数字是多少对齐数就是多少。枚举:一个定义常量的集合;
默认里面所列举出来的第一个值是零,如果第一个所举出来的值给5,则表示从5往后走;
enum
和int
是否应该划上等号呢?在C语言之中,enum
和int
是等价的,搞个枚举的目的只是为了代码的可读性能够更好一点;而enum
所表示出来的含义,则就是我们所说到的枚举出来的概念,整个概念是不应该和整数进行划等号的;而枚举在内存之中的存储和int
一样的,都是以字节序+原码反码补码的形式来进行存储。
一种特殊的自动类型,里面的这些成员,共用一块空间,谁大就用谁的空间,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。(最为特殊的例子就是对于ip的使用)
位段的内存布局是和平台强相关的,并没有统一的标准。位段更多的在网络通信的时候会进行应用,
struct A {
int _a:2;// _a这个变量只占2个bit位
int _b:5;
int _c:10;
int _d:30;
strlen()
的时候 \0 是结束标志,不算作字符串内容。一个字符数组如果没有\0 就不是字符串,就不能够使用strlen。strlen
字符串是以’\0’来作为结束标志的 strlen函数返回的是斜杠0前面的出现的字符个数,不包括\0 (sizeof是一个运算符)。strlen
计算长度函数strlen
的使用
char *p1 = "hello world";
printf("%d\n", strlen(p1));
char p2[] = "hello world"; //char p2[] = "hello\\0world"; 则长度为12 大小为13
//p2[5] = '0'; 结果还为11
//p2[5] = '\0';结果为5
printf("%d\n", strlen(p2));//结果也为11 但此数组的大小是12
printf("%d\n", sizeof(p2));
strcpy
拷贝函数拷贝函数的实现
char* my_strcpy(char*dst, const char* src)
{
assert(dst != NULL);
assert(src != NULL);
char* ret = dst;
while (*src != '\0')
{
*dst = *src;
++src;
++dst;
}
*dst = '\0';
return ret;
}
strcat
追接/拼接函数#include
#include
#include
// strcat zhuijia pinjie
char* my_strcat(char* dst, const char* src){
assert(dst&&dst);
char *ret = dst;
while (*dst != '\0')
{
++dst;
}
while (*dst++ = *src++);//运算符
//while (*src != '\0'){
// *dst = *src;
// ++dst;
// ++src;
//}
//*dst = *src;
return ret;
}
int main(){
char *p1 = "hello";
char p2[11] = "world";
//strcpy(p2,p1);
strcat(p2, p1);
return 0;
}
strcmp
比较函数Strcmp
所比较字符串每个字符的ASCII码
代码的实现
int my_strcmp(const char* str1, const char* str2){
assert(str1&&str2);
unsigned char* s1 = (unsigned char*)str1;
unsigned char* s2 = (unsigned char*)str2;
while (*s1&&*s2)
{
if (*s1 > *s2)
{
return 1;
}
else if (*s1 < *s2)
{
return -1;
}
else {
return 0;
}
}
if (*s1 == '\0'&&*s2 == '\0')
{
return 0;
}
else if(*s1=='\0')
{
return -1;
}
else
{
return 1;
}
return 0;
}
int main(){
char *p1 = "hello";
char *p2 = "world";
printf("%d\n", my_strcmp(p1, p2));// 大于1 小于-1 等于0
return 0;
}
strncpy strncat strncmp
进行具体数字的操作#include
#include
#include
int main(){
char *p1 = "hellohello";
char p2[100] = "world";
strncpy(p2, p1, 5);
printf("%s\n", p2);
strncat(p2, p1, 5);
strncmp(p2, p1, 5);
return 0;
}
strstr
比较一个字符串中是否存在另一个字符串函数int main(){
char *p1 = "abcde";
char *p2 = "deabc";
char p3[11];
strcpy(p3, p1);
strcat(p3, p1);
if (strstr(p3, p2) != NULL)
{
printf("是旋转字符串\n");
}
else
{
printf("不是旋转字符串\n");
}
return 0;
}
实现函数
const char* my_strstr(const char* src, const char* sub){
assert(src&&sub);
const char* srci = src;
const char* subi = sub;
while (*srci!='\0')
{
while (*srci==*subi&&*subi!='\0')
{
++srci;
++subi;
}
if (*subi=='\0')
{
return src;
}
else
{
subi = sub;
++src;
srci = src;
}
}
return NULL;
}
#include
#include
#include
#include
// memcpy 所有的拷贝
void *my_memcpy(void*dst, const void *src, size_t num)
{
assert(dst&&src);
char * str_dst = (char*)dst;
char * str_src = (char*)src;
for (size_t i = 0; i < num; i++)
{
str_dst[i] = str_src[i];
}
return dst;
}
void *my_memmove(void*dst, const void *src, size_t num)
{
assert(src&&dst);
char * str_dst = (char*)dst;
char * str_src = (char*)src;
if (str_src < str_dst&&str_dst<str_src+num)//后重叠,或者不重叠,从后往前拷
{
for (int i = num - 1; i >= 0; --i)
{
str_dst[i] = str_src[i];
}
}
else// 前重叠 不重叠 从前往后拷
{
for (size_t i = 0; i < num; ++i)
{
str_dst[i] = str_src[i];
}
}
return dst;
}
正数:原码是其本身 反码 补码和原码是完全相同的
7 原码 0000 0111
反码 0000 0111
补码 0000 0111
负数:原码是其本身 反码符号位不变,其余位全部取反,补码则是在反码的基础上加1
-7 原码 1000 0111
反码 1111 1000
补码 1111 1001
零:零分为正零+0 和负零 -0
+0 原码 0000 0000
反码 0000 0000
补码 0000 0000
-0 原码 1000 0000
反码 1111 1111
补码 0000 0000
补码往回转的话,依然是取反+1 切记!!!
大端是和我们的认知是相符合的,则是从低到高开始进行排列。
小端的话则和我们的认知相反,它是从小往大的排列。
一个代码检测自己的电脑到底是大端序还是小端序
#include
int main(){
int x=1;
char c=(char)x;
if(c==1)
{
printf("小端机\n");
}
else if(c==0)
{
printf("大端机\n");
}
return 0;
}
ASCII 是我们所看见的字母和数字在内存之中所在的十进制地址值
Int* a=int(*)malloc(sizeof(int)*100); 申请100个空间不想用的话就还他自由 free(a);
calloc void* calloc(size_t num, size_t size);把num个大小为size的元素开辟一个空间,值全部为0
void* realloc(void* ptr,size_t size); 把一个曾经已经申请到的内存进行扩容
Realloc 扩容 重新开个大的,将原来的内容拷贝过去,之后将原来的空间释放(后面如果有足够地空间,直接扩容,如果没有就找足够地空间)
//2进制
fwrite(&a, sizeof(int), 1, fin);
//文本
fputs("10000", fin);
FILE* fout = fopen("learn3_4_1.c", "r");
// 读
char ch = fgetc(fout);
while (ch != EOF)
{
printf("%c", ch);
ch = fgetc(fout);
}
//清空写
FILE* fin = fopen("learn3_4_1.c", "w");
fputc('h', fin);
fputc('e', fin);
fputc('l', fin);
fputc('l', fin);
fputc('o', fin);
//追加写
FILE* fain = fopen("learn3_4_1.c", "a");
fputs("#include " , fain);
fputs("int main{", fain);
fputs("return 0;", fain);
fputs("}", fain);
//定位
FILE* fdin = fopen("file.txt", "r+");
char ch = fgetc(fdin);
while (ch != EOF)
{
if (ch == '/')
{
char next = fgetc(fdin);
if (next == '/')
{
fputc('*',fdin);
}
}
ch = fgetc(fdin);
}
fclose(fdin);
}
int main() {
FILE* fout = fopen("test.txt", "r");
fseek(fout, 5, SEEK_SET);//设置的文件的开始
//fseek(fout, 0, SEEK_END);设置的文件的末尾
//fseek(fout, 5, SEEK_CUR);//跳过几个位置
//fseek(fout, 5, SEEK_CUR);//跳过几个位置
//1.读前五个后跳过五个
//char out_ch = fget(fout);
//for(int i=0;i<5;i++){
//}
// 之后再跳5个
//2. 算文件的大小
//fseek(fout, 0, SEEK_END);设置的文件的末尾
//printf("文件的大小:%d", ftell(fout));// 计算文件大小
//3.判断文件结束 如果没读到结尾会返回非零
char ch = fget(fout);
while (ch != EOF) {
printf("%c", ch);
ch = fgetc(fout);
}
if (feof(fout) != 0) {
printf("read end of file\n");
}
/*if (ferror(fout) != 0) {
printf("read error");
}*/
//4. 读视频
fseek(fout, 0, SEEK_END); //设置的文件的末尾
long long n = ftell(fout);
printf("%d\n",n);
fseek(fout, 0, SEEK_SET); //设置的文件的kaishi
while (n--) {
printf("%c", ch);
ch = fgetc(fout);
}
fclose(fout);
system("pause");
return 0;
}
#ifdef __DEBUG__//有这样的就编译,没有就不编译
printf("%d\n", arr[i]);
#endif//
#include
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
void*
是一种特殊的指针类型,只有对应的地址,没有内存的大小。
指针的类型决定了指针向前或者是向后走一步有多大(距离)。
#include
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
char *str3 = "hello bit.";
char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
其中:栈越往下地址越小,常量区的数据不能够修改。
str1 str2
这两者都是数组,在栈中会开数组长度的一个大小,将数组内容拷贝进去,而他们之中所存放的是首字母的地址,两者在栈之中地址是不同的,后进先出的原则,后面的地址会小一些。
str3 str4
这两者是指针 则只会开4个字节,存放的是首字母h的地址,两者是相同的。
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
既然可以把数组名当成地址存放到一个指针中,那么我们就可以使用指针来访问一个数组。
#include
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
通过打印结果我们可以发现,p+i
其实所计算的就是数组arr
下标为i的地址,因此我们就可以直接通过指针来访问数组。
int arr[10];
之中的,arr
和&arr
分别是啥?#include
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
通过运行结果我们可以看的出来,两者所打印出来的地址是完全相同的,但是两者所表示的意义却是完全不同的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针。
int *p1[10];
int (*p2)[10];
判断是数组指针,还是指针数组:
[]
的优先级高一些。()
和 [ ]
是从左到右看!! 先确定的写在最后面(也就是按优先级顺序开始读,但是是从后往前开始书写)!!指针数组是指针还是数组?
答案:数组,是存放指针的数组
int* arr[5];//是什么?
arr是一个数组,有五个元素,每个元素是一个整形指针。
数组指针是指针?还是数组?
答案是:指针
//函数指针的定义和调用
typedef void(*P_FUNC)(int);
void f1(int a){
printf("wolaile:%d\n",a);
}
void (*signal(int ,void(*)(int)))(int);
P_FUNC signal(int, P_FUNC);
int main(){
void(*pf1)(int);
P_FUNC pf2=f1;
pf1(1);
pf2(2);
(*pf1)(3);
(*pf2)(4);
system("pause");
return 0;
}
const char* name=“chen”
name被定义为指向常量的指针,所以它所指的内容不能改变,但指针本身的内容可以修改,而name[3]=‘q’
,修改了name所指的内容,是错误的;name==lin,name=new char[5],name=new char('q')
以不同的方法修改了常指针,都是正确的。
(收藏起来之后慢慢看)
写在完结处的话:再一次重新整理后从原本的42426字改成了24818字,也是因为自己对于C语言的更加娴熟,因此对于一些杂糅的东西可以做到删除或者是增添,这样也能够更加基础和宽泛的将涉及到的众多基础和升华部分容做一次全面的整合,是帮助自己也是帮助别人,那些正在学习路上的朋友,未来的路还长一定要记得加油。