C语言核心知识点Day05
1.结构体嵌套二级指针
当创建结构体变量后,如果其中的成员为指针类型的话,就要将此变量
开辟至堆空间,是因为在创建结构体后,程序在运行结束之后,对于结构体
变量所占的内存空间就已经分配好了,分配的大小为4字节(也就是一个指针
的)大小,如果不重新在堆空间中开辟内存的话,就会导致溢出,因此,一
般情况下,看到使用指针类型的数据的时候,应该在堆空间中存储。同样的,当存在多级指针的时候,也就相应的要开辟多块存储空间,在
这个时候,记住先释放内层的堆空间,再释放外层的堆空间。小技巧:浏览代码看malloc数和free数是否相等,如果相等的话一般都是
没问题的。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>struct Teacher
{char* name;char** student;
};void test01()
{struct Teacher** t1 = malloc(sizeof(struct Teacher* ) * 3);//开辟空间并进行赋值操作for (int i = 0; i < 3; i++){t1[i] = malloc(sizeof(struct Teacher));t1[i]->name = malloc(sizeof(char) * 64);//给每个教师的名字开辟堆空间sprintf(t1[i]->name, "teacher_%d", i + 1);t1[i]->student = malloc(sizeof(char*) * 5);//每个老师可以带5个学生for (int j = 0; j < 5; j++){t1[i]->student[j] = malloc(sizeof(char) * 64);sprintf(t1[i]->student[j], "teacher_%d_name_%d", i + 1, j + 1);}}//打印输出结果for (int i = 0; i < 3; i++){printf("teacher_name = %s\n", t1[i]->name);for (int j = 0; j < 5; j++){printf("studennt_name = %s\t", t1[i]->student[j]);}printf("\n");}//释放空间for (int i = 0; i < 3; i++){if (NULL != t1[i]->name){free(t1[i]->name);t1[i]->name = NULL;}for (int j = 0; j < 5; j++){if (t1[i]->student[j] != NULL){free(t1[i]->student[j]);t1[i]->student[j] = NULL;}}if (NULL != t1[i]){free(t1[i]);t1[i] = NULL;}}if (NULL != t1){free(t1);t1 = NULL;}
}int main()
{test01();return 0;
}
2.结构体成员偏移量
计算结构体中的成员偏移量的时候有两种办法:1.通过计算不同数据类型所占的字节大小累计后得出目标成员的偏移量。2.通过头文件<stddef.h>中的offsetof函数来进行计算
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stddef.h>struct Teacher
{char a;int b;
};void test01()
{ struct Teacher t1;struct Teacher *p = &t1;printf("结构体中的b的偏移量:%d\n", (int)&(p->b) - (int)p);printf("结构体中的b的偏移量:%d\n", offsetof(struct Teacher,b));
}void test02()//通过偏移量 访问内存
{struct Teacher t1 = {'c',22};struct Teacher* p = &t1;printf("结构体中的b的值:%d\n",*(int* )((char* )p + offsetof(struct Teacher, b)));printf("结构体中的b的值:%d\n", *((int*)p + 1));}
//结构体
struct teacher2
{char a;int b;struct Teacher c;
};
void test03()
{struct teacher2 t2= { 'a',10,'b',20 };int offset1 = offsetof(struct teacher2, c);int offset2 = offsetof(struct Teacher, b);//方式一:printf("属性C中的值为:%d\n", *(int*)((char*)&t2 + offset1 + offset2));//方式二:printf("属性C中的值为:%d\n", ((struct Teacher*)((char*)&t2 + offset1))->b);
}int main()
{//test01();//test02();test03();return 0;
}
3.结构体字节对齐
在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中
所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是
事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这
就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这
就是内存对齐。举一个很简单的例子,例如我们在内存中存储数字,假如是按照我们预期
的顺序存储的方式进行存储,存储一个char 和一个int型的数据,假设计算机
在读取的时候是4字节读取,那么就会先读到char类型的数据,然后读取int型
数据的前三位,这是一次读取,然后再读取下一块内容,读取int型数据的最
后一位数据,最后如果要得到这个数据,计算机还要进行拼接操作,将之前
所读取到的三个字节和第二次读取到的一个字节拼接起来,才可以得到完整
的数字,而通过字节对齐的方式进行存储,虽然在空间上浪费,但是在时间
上却大大提高了效率,因此,这是一种牺牲空间换取时间的存储方式,提高
了存取数据的效率。对齐的原则为以下几点:1.第一个属性开始,从零开始计算偏移量2.第二个属性要放在,该属性大小与对齐模数比,二者较小的那个值的整数倍当所有属性计算完毕后,整体做二次偏移,将上有计算的结果,扩充到这个结构体中最大的数据类型的整数倍上也就是说,其实对齐就是以结构体中最大的数据类型为基准,其余的变量的存储都是以这个最大的数据类型的空间为准,能存储的下就存储,存储不下就开辟一块新的最大的数据类型的空间进行存储。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>#pragma pack(show)//对齐模数 8//内存对齐原则
//第一个属性开始,从零开始计算偏移量
//第二个属性要放在 该属性大小与对齐模数比,二者较小的那个值的整数倍
//当所有属性计算完毕后,整体做二次偏移,将上有计算的结果,扩充到这个结构体中最大的数据类型的整数倍上typedef struct _STUDENT
{int a;char b;double c;float d;
}Student;void test01()
{printf("student结构体的大小为:%d\n", sizeof(Student));
}//结构体嵌套结构体的时候
typedef struct _STUDENT2
{char a;//8Student b;//24double c;//40
}Student2;void test02()
{printf("student2结构体的大小为:%d\n", sizeof(Student2));
}int main()
{test01();test02();return 0;
}
4.文件
4.1 流的概念流是一个动态的概念,可以将一个字节形象地比喻成一滴水,字节在
设备、文件和程序之间的传输就是流,类似于水在管道中的传输,可以看
出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。C语言
中,I/O操作可以简单地看作是从程序移进或移出字节,这种搬运的过程
便称为流(stream)。4.2 文件指针 我们知道,文件是由操作系统管理的单元。当我们想操作一个文件的
时候,让操作系统帮我们打开文件,操作系统把我们指定要打开文件的信
息保存起来,并且返回给我们一个指针指向文件的信息。文件指针也可以
理解为代指打开的文件。这个指针的类型为FILE类型。该类型定义在stdio.h
头文件中。通过文件指针,我们就可以对文件进行各种操作。在VS中,文件指针的结构体如下所示:
struct _iobuf { char *_ptr; //文件输入的下一个位置 int _cnt; //剩余多少字符未被读取char *_base; //指基础位置(应该是文件的其始位置) int _flag; //文件标志 int _file; //文件的有效性验证 int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取 int _bufsiz; //文件的大小 char *_tmpfname; //临时文件名
};
typedef struct _iobuf FILE;
4.3 文件缓冲区在文件读取的过程中,其实都是通过文件缓冲区,来建立文件和操作
系统或者是操作系统和文件之间的互相传输内容,在开始读取的过程中,
都是先将读取的内容传输到流中,当流中内存占满时才进行下一步的输入或者
输出的操作这个过程既可以提高读取的速度,同时也可以提升我们硬盘的寿
命,因为每一次的读取过程都会减少硬盘的寿命,这样可以大大提升硬盘的
寿命。文件操作完成后,如果程序没有结束,必须要用fclose()函数进行关闭,
这是因为对打开的文件进行写入时,若文件缓冲区的空间未被写入的内容填
满,这些内容不会写到打开的文件中。只有对打开的文件进行关闭操作时,
停留在文件缓冲区的内容才能写到该文件中去,从而使文件完整。再者一旦
关闭了文件,该文件对应的FILE结构将被释放,从而使关闭的文件得到保护
,因为这时对该文件的存取操作将不会进行。文件的关闭也意味着释放了该
文件的缓冲区。
4.4 文件的读写4.4.1 文件读写的几种不同方式:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>//1、字符的读写回顾 fgetc(), fputc()
void test01()
{//写文件FILE * f_write = fopen("./test1.txt", "w");if (f_write == NULL){return;}char buf[] = "hello world";for (int i = 0; i < strlen(buf);i++){fputc(buf[i], f_write);}fclose(f_write);//读文件FILE * f_read = fopen("./test1.txt", "r");if (f_read == NULL){return;}char ch;while ( (ch = fgetc(f_read)) != EOF) // EOF END OF FILE{printf("%c", ch);}fclose(f_read);
}//2、按行进行读写
void test02()
{//写文件FILE * f_write = fopen("./test2.txt", "w+");if (f_write == NULL){return;}char * buf[]={"锄禾日当午\n","汗滴禾下土\n","谁知盘中餐\n","粒粒皆辛苦\n"};for (int i = 0; i < 4;i++){fputs(buf[i], f_write);}fclose(f_write);//读文件FILE * f_read = fopen("./test2.txt", "r");if (f_read == NULL){return;}while ( !feof(f_read)){char temp[1024] = { 0 };fgets(temp, 1024, f_read);printf("%s", temp);}fclose(f_read);}//3、按块进行读写struct Hero
{char name[64];int age;
};
void test03()
{//写文件FILE * f_wirte = fopen("./test3.txt", "wb"); if (f_wirte == NULL){return;}struct Hero heros[] = {{ "孙悟空", 999 },{ "亚瑟", 20 },{ "曹操", 80 },{ "鲁班", 5 },};for (int i = 0; i < 4;i++){//参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针fwrite(&heros[i], sizeof(struct Hero), 1, f_wirte);}fclose(f_wirte);//读文件FILE * f_read = fopen("./test3.txt", "rb");if (f_read == NULL){return;}struct Hero temp[4];fread(&temp, sizeof(struct Hero), 4, f_read);for (int i = 0; i < 4;i++){printf("姓名: %s 年龄:%d\n", temp[i].name, temp[i].age);}fclose(f_read);}//4、格式化读写回顾
void test04()
{//写文件FILE *f_write = fopen("./test4.txt", "w");if (f_write == NULL){return;}fprintf(f_write, "hello world %s", "abcd");fclose(f_write);//读文件FILE * f_read = fopen("./test4.txt", "r");if (f_read == NULL){return;}char temp[1204] = { 0 };while (! feof(f_read )){fscanf(f_read, "%s", temp);printf("%s\n", temp);}fclose(f_read);
}void test05()
{//写文件FILE * f_wirte = fopen("./test5.txt", "wb");if (f_wirte == NULL){return;}struct Hero heros[] ={{ "孙悟空", 999 },{ "亚瑟", 20 },{ "曹操", 80 },{ "鲁班", 5 },};for (int i = 0; i < 4; i++){//参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针fwrite(&heros[i], sizeof(struct Hero), 1, f_wirte);}fclose(f_wirte);//读文件FILE * f_read = fopen("./test51.txt", "rb");if (f_read == NULL){//error 宏//printf("文件加载失败\n");perror("文件加载失败"); //用户提示信息 + 系统提示信息return;}struct Hero tempHero;//移动光标// 参数1 文件指针 参数2 偏移大小 参数3 起始位置 // SEEK_SET 从开始 SEEK_END 从结尾 SEEK_CUR 从当前位置//fseek(f_read, sizeof(struct Hero) * 2, SEEK_SET);fseek(f_read, - (long)sizeof(struct Hero) * 2, SEEK_END);rewind(f_read); //将文件光标置首fread(&tempHero, sizeof(struct Hero), 1, f_read);printf(" 姓名: %s , 年龄: %d\n", tempHero.name, tempHero.age);fclose(f_read);
}int main(){//test01();//test02();//test03();//test04();test05();system("pause");return EXIT_SUCCESS;
4.4.2 文件读写的注意事项1.在进行单个字符(例如:getc、putc等函数的调用的时候)为什么不在
读取单个字符的时候使用feof来进行判断是否到文件末尾呢?是因为fetc会使光标移动至下一个位置,因此会移动到EOF位置,并将
EOF打印出来后再进行判断。解决办法如下所示的if代码
void test01()
{//读文件FILE* fp1 = fopen("./test01.txt", "r+");if (NULL == fp1)return;char ch;//while ((ch = fgetc(fp1))!=EOF)//{// printf("%c", ch);//}while(!feof(fp1)){//为什么不在读取单个字符的时候使用feof判断呢//是因为fetc会使光标移动至下一个位置,因此会移动到EOF位置,并将EOF打印出来后再进行判断//解决办法如下所示的if代码ch = fgetc(fp1);if (ch == EOF)break;printf("%c", ch);}fclose(fp1);
}
2.如果结构体中的有一个变量是指针类型,要将数据创建在堆区;也
就是说不要将这个指针变量存储在文件中。4.5 配置文件两种版本,一种是自己写的一种是网课上的,对比差距在代码易读性以及
对于结构体的熟练适应程度上有很大的差距 。1.自己写的版本
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>int getFilelines()
{FILE* fp = fopen("test07.txt", "r");if (fp == NULL)return;int lines = 0;char buffer[1024] = { 0 };while (!feof(fp)){memset(buffer, 0, 1024);fgets(buffer, 1024, fp);if (buffer[0] != '#' && strstr(buffer, ":") != 0)lines++;}return lines;
}void getFilevalue(char** p)
{FILE* fp = fopen("test07.txt", "r");if (fp == NULL)return;char buffer[1024] = { 0 };int index = 0;while (!feof(fp)){memset(buffer, 0, 1024);fgets(buffer, 1024, fp);if (buffer[0] != '#' && strstr(buffer,":") != 0){p[index] = malloc(sizeof(char) * 64);memset(p[index], 0, 64);for (int i = 0; i < strlen(buffer); i++){p[index][i] = buffer[i];}index++;}}
}void findfileValue(char** p, int a)
{printf("请输入您要查询的值:\n");char inputArr[64] = { 0 };scanf("%s", inputArr);//根据键值来匹配相应的内容int i = 0;while (i < a){if (strncmp(p[i], inputArr, strlen(inputArr)) == 0){strtok(p[i], ":");printf("%s ", strtok(NULL, ":"));break;}++i;}if (i == a)printf("无输入指定内容!");
}void freeSpace(char** p,int a)
{for (int i = 0; i < a; i++){if (p[i] != NULL){free(p[i]);p[i] = NULL;}}free(p);p = NULL;
}void test01()
{//获取行数int a = getFilelines();//将内容保存到数组中char** p = malloc(sizeof(char*) * a);getFilevalue(p);//用户查询findfileValue(p,a);//释放开辟的空间freeSpace(p, a);
}int main()
{ test01();return 0;
}
2.别人的版本
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>struct ConfigInfo
{char key[64]; //索引值char value[64]; //实值
};//获取有效行数
int getFileLine( char * fileName );//判断当前行是否有效
int isValidLine(char * str);//解析文件
void parseFile(char * filePath, int lines, struct ConfigInfo ** configInfo);//根据索引值 获取 实值
char * getInfoByKey(char * key, struct ConfigInfo * configInfo, int line);//释放信息
void freeSpace(struct ConfigInfo * configInfo);
struct Person
{char a;int b;
};void test01()
{char * filePath = "./config.txt";int line = getFileLine(filePath);printf("文件的有效行数为:%d\n", line);struct ConfigInfo * pArray = NULL;parseFile(filePath, line, &pArray);//测试 根据key 访问valueprintf("heroId = %s\n", getInfoByKey("heroId", pArray, line));printf("heroName = %s\n", getInfoByKey("heroName", pArray, line));printf("heroAtk = %s\n", getInfoByKey("heroAtk", pArray, line));printf("heroDef = %s\n", getInfoByKey("heroDef", pArray, line));printf("heroInfo = %s\n", getInfoByKey("heroInfo", pArray, line));//释放内存freeSpace(pArray);pArray = NULL;int main(){test01();system("pause");return EXIT_SUCCESS;
}//获取有效行数
int getFileLine(char * fileName)
{FILE * file = fopen(fileName, "r");if (file == NULL){return -1 ;}char buf[1024] = { 0 };int lines = 0;while (fgets(buf, 1024, file) != NULL){//如果是有效行 才统计if (isValidLine(buf)){lines++;}}fclose(file);return lines;
}//判断当前行是否有效
int isValidLine(char * str)
{if (str[0] == ' ' || str[0] == '\0' || strchr(str,':') == NULL){return 0; //无效数据 都返回假}return 1;
}//解析文件
void parseFile(char * filePath, int lines, struct ConfigInfo ** configInfo)
{struct ConfigInfo * info = malloc(sizeof(struct ConfigInfo) * lines);if (info == NULL){return;}FILE * file = fopen(filePath, "r");char buf[1024] = { 0 };int index = 0;while ( fgets(buf,1024,file ) != NULL){//解析数据 有效数据才解析// heroName:aaaa\nif (isValidLine(buf)){memset(info[index].key, 0, 64);memset(info[index].value, 0, 64);char * pos = strchr(buf, ':'); //pos代表冒号所在位置strncpy(info[index].key, buf, pos - buf); //将key截取到 结构体中 最后-1的原因是不需要截取换行符strncpy(info[index].value, pos + 1, strlen(pos + 1) - 1);/*printf("key = %s\n", info[index].key);printf("value = %s", info[index].value);*/index++;}memset(buf, 0, 1024);}*configInfo = info;
}//根据索引值 获取 实值
char * getInfoByKey(char * key, struct ConfigInfo * configInfo, int line)
{for (int i = 0; i < line;i++){if ( strcmp (key, configInfo[i].key ) == 0){return configInfo[i].value;}}return NULL;
}//释放信息
void freeSpace(struct ConfigInfo * configInfo)
{if (configInfo != NULL){free(configInfo);configInfo = NULL;}}
4.6 简单的加密解密文件简单的加密的过程就是将文件中的内容转化为单个字符,然后对单个字符
进行变化的过程,例如进行逻辑运算或者是加减运算,从而转换文件的具体
内容,达到加密的效果。简单的解密的过程则于上述恰好相反,对于加密过程中的加减运算一
般是进行逆运算,对于逻辑运算则是通过左移或者是右移的方式进行还原,
下面自己写的一个简单的加密解密过程:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>void passwdFile()
{FILE* fp = fopen("passwdfile.txt", "r");FILE* tempfp = fopen("temppasswdfile.txt", "w");if (fp == NULL || tempfp == NULL){perror("文件打开失败!");return;}char buffer[1024] = { 0 };while (!feof(fp)){memset(buffer, 0, 1024);fgets(buffer, 1024, fp);for (int i = 0; i < strlen(buffer); ++i){buffer[i] += 1;buffer[i] ^= buffer[i];}fputs(buffer, tempfp);}fclose(fp);fclose(tempfp);remove("passwdfile.txt");rename("temppasswdfile.txt", "passwdfile.txt");
}void openpasswdFile()
{FILE* fp = fopen("passwdfile.txt", "r");FILE* tempfp = fopen("temppasswdfile.txt", "w");char buffer[1024] = { 0 };while (!feof(fp)){memset(buffer, 0, 1024);fgets(buffer, 1024, fp);for (int i = 0; i < strlen(buffer); ++i){buffer[i] -= 1;}fputs(buffer, tempfp);}fclose(fp);fclose(tempfp);remove("passwdfile.txt");rename("temppasswdfile.txt", "passwdfile.txt");
}//文件加密(关于逻辑运算的)
// 将#(35)转为 short,即将字符对应的ASCII码转换成short型
// 0000 0000 0010 0011 << 4(左移)
// 0000 0010 0011 0000
// 1000 0000 0000 0000 |(或运算,最高位置1,也就是符号位取负)
// 1000 0010 0011 0000 +(低位加任意 0000 ~ 1111 随机数,用rand())
// 1000 0010 0011 1010//解密
// 1000 0010 0011 1010 <<1 //左移一位
// 000 0010 0011 10100 >> 5//右移五位
// 0000 0000 0010 0011void test01()
{//加秘文件passwdFile();//解密文件openpasswdFile();
}int main()
{test01();return 0;
}
C语言核心知识点Day05相关推荐
- C语言核心知识点大汇总
C语言高级部分总结 一.内存 内存就是程序的立足之地,体现内存重要性. 内存理解: 内存物理看是有很多个Bank(就是行列阵式的存储芯片),每一个Bank的列就是位宽 ,每一行就是Words,则存 ...
- Go语言核心知识点和原理详解
go核心原理 本人在一家go技术栈工作2年有余,因此梳理一下我认为比较重要的go语言技术知识,一些基础的概念,比如function, interface这些就忽略了. https://dravenes ...
- 【C】C语言核心知识点总结(Reference Manual)
1.词法 (1)字符集-- C语言字符集使用ISO/IEC 10646基本拉丁字符集,包括52个大小写拉丁字母,10个数字,空格,水平制表符HT,垂直制表符VT,换页符FF,以及29个特殊字符,它们是 ...
- C语言核心知识点Day07
1.预处理 1.1 预处理的基本概念C语言对源程序处理的四个步骤:预处理.编译.汇编.链接.预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理.这个过程并不对 ...
- 10分钟学会 SQL 语言核心知识点!
在Java Web开发中必不可少的就是 SQL 语言!万能的 SQL 语言可以使你自由的徜徉于各种数据库的海洋中.而学习 SQL 语言一点都不困难,小编整理了整理了 SQL 语言常用的语法和命令,要让 ...
- C语言核心知识点总结
文章目录 一.数据类型 1.为什么出现这么多类型? 数据的存放方式 常量与变量 1.作用域 2.变量的生命周期 3.常量 数组 字符串 1.二维数组 指针 1.指针数组 2.数组指针 3.总结 指针进 ...
- 1563页Go语言中文文档,涵盖Go语言所有核心知识点,限时免费下载!
Go 语言有多火爆?国外如 Google.AWS.Cloudflare.CoreOS 等,国内如七牛.阿里等都已经开始大规模使用 Go 语言开发其云计算相关产品. Go 语言的语法很简单,很容易掌握. ...
- java 重用性_Java开发重用性必备的三大核心知识点
互联网开发行业的人在问我提高java代码可重用性有哪些方法措施,那今天我就给大家详细讲下提高java代码可重用性的措施吧,希望不懂的可以从中学习加以理解,懂的也可以加以巩固下这些知识点,我把提高jav ...
- Dubbo核心知识点
本文来说下Dubbo核心知识点,也是面试中的重难点. 文章目录 Dubbo是什么 RPC又是什么 说说Dubbo的分层 能说下Dubbo的工作原理吗 为什么要通过代理对象通信 说说服务暴露的流程 说说 ...
最新文章
- Rman--备份命令
- VIM使用系统剪切板
- 基于报文地址的策略路由配置示例
- python调用其他程序或脚本方法(转)
- .Net中委托的协变和逆变详解
- 在windows下安装node-sass失败,提示\node-sass: Command failed,解决方案
- Android通过webservice连接SQLServer 详细教程(数据库+服务器+客户端)
- 2022年最新的西安Java培训机构十大排名榜单
- 苹果cookie是打开还是关闭_如何避免苹果safari自带浏览器“跟踪”你的信息!
- 用GNS3制作路由交换网络拓扑图
- vmware启动虚拟机报“内部错误”的解决方法
- java 身份证第18位数字的算法
- Pikachu漏洞练习平台----验证码绕过(on server) 的深层次理解
- 模具设计:模具做的好,镶件也要镶得好!
- Nginx具体配置(三)
- symbian与uiq开发教程
- SOM网络算法分析与应用(适合入门、快速上手)
- java 代码块(静态代码块、局部代码块、构造代码块)
- 今晚8点和 Conflux 张元杰聊聊,在中国做 Web3 的机会在哪里?
- 我国经济最强的城市是哪个?北京、上海、还是深圳?