C语言标准库

C语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
C标准库是一组 C 内置函数、常量和头文件,比如 stdio.hstdlib.hmath.h 等等。这个标准库可以作为 C 程序员的参考手册。
C语言标准库有各种不同的实现,比如glibc, 用于嵌入式Linux的uClibc,还有ARM公司的C语言标准库及精简版的MicroLib等。不同标准库的实现并不相同,而且提供的函数也不完全相同,不过有一个它们都支持的最小子集,这也就是最典型的C语言标准库。
典型的C语言标准库(GNU C 标准库)官网:The GNU C Library

glibc和libc有什么区别呢?
libc是Linux下的ANSI C的函数库;
glibc是Linux下的GUN C函数库;
ANSI C和GNU C有什么区别呢?
ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。这个库可以根据 头文件划分为 15 个部分,其中包括:字符类型 (<ctype.h>)、错误码 (<errno.h>)、 浮点常数 (<float.h>)、数学常数 (<math.h>)、标准定义 (<stddef.h>)、 标准 I/O (<stdio.h>)、工具函数 (<stdlib.h>)、字符串操作 (<string.h>)、 时间和日期 (<time.h>)、可变参数表 (<stdarg.h>)、信号 (<signal.h>)、 非局部跳转 (<setjmp.h>)、本地信息 (<local.h>)、程序断言 (<assert.h>) 等等。这在其他的C语言的IDE中都是有的。
而GNU C函数库是一种类似于第三方插件的东西,由于Linux是用C语言写的,所以Linux的一些操作是用C语言实现的,所以GNU组织开发了一个C语言的库 用于我们更好的利用C语言开发基于Linux操作系统的程序。其实我们可以把它理解为类似于Qt是一个C++的第三方函数库一样。

标准库 内容
<stdio.h> 输入和输出
<stdlib.h> 最常用的一些系统函数
<string.h> 字符串处理
<math.h> 定义了一系列数学函数
<ctype.h> 包含了一系列字符类测试函数
<time.h> 时间和日期
<stdarg.h> 可变参数列表
<signal.h> 信号
<assert.h> 声明断言
<setjmp.h> 非局部跳转
<errno.h> 定义错误代码和处理错误的函数
<stddef.h> 一些常数、类型和变量
<locale.h> 定义了特定地域的设置,比如日期格式和货币符号
<float.h> 包含了一组与浮点值相关的常量和运算
<limits.h> 定义整数数据类型的取值范围

C语言错误处理和调试

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式提供底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

C语言错误处理一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;

int main (){
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL){
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}else{
fclose (pf);
}
return 0;
}

C语言调试

Linux下的C语言程序调试,可以使用GDB。这篇文章记录了GDB调试器基本使用,更多的调试技术,后续再研究。

C语言输入输出

getchar() & putchar()

一次只能输入输出一个字符。
Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main(){
int c;

printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");

char *s = "abc";
int i =0;
for ( i=0;i<sizeof(s);i++){
putchar(s[i]);
putchar('\n');
}

return 0;
}

gets() & puts()
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

Example:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(){
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}

scanf() 和 printf()
int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {

char str[100];
int i;

printf( "Enter a value :");
scanf("%s %d", str, &i);

printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}

余生疯狂做产品!跟这个世界继续死磕!

今晚复习跟数据结构息息相关的几个知识点。

存储类型

C语言中有以下类型的存储方式:

  • auto
    • auto用于声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。
    • 因为auto是C语言变量默认的存储类型,所以一般不写。
  • register
    • register要求编译器尽可能的将register类型的变量存在CPU内部寄存器中而不是通过内存寻址的RAM中,以便提高效率。
  • static
    • static指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。使用 static修饰局部变量可以在函数调用之间保持局部变量的值。
    • static修饰符也可以应用于全局变量,当 static修饰全局变量时,会使变量的作用域限制在声明它的文件内。
    • static是全局变量的默认存储类。
  • extern
    • extern存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用extern时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
    • 有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用extern来得到已定义的变量或函数的引用。extern是用来在另一个文件中声明一个全局变量或函数。
  • const
    • const要求其所修饰的对象为常量,不可对其修改和二次赋值操作(不能作为左值出现)。
  • volatile 修饰符
    • volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

C语言const与static的区别

  • const 就是只读,只在声明中使用;
  • static 一般有两个作用,规定作用域和存储方式。

对于局部变量, static规定其为静态存储方式, 每次调用的初始值为上一次调用的值,调用结束后存储空间不释放;
对于全局变量, 如果以文件划分作用域的话,此变量只在当前文件可见; 对于static函数也是在当前模块内函数可见;
static const 是上面两者的合集。

结构体

结构体的声明方式:

1
2
struct 结构体名
{成员列表};

结构体声明样例:

1
2
3
4
5
6
7
8
9
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

声明的结构体相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元,为了能在程序中使用结构类型的数据,应当定义结构体类型的变量,并在其中存放具体的数据。

结构体的嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct date{
int month;
int day;
int year;
}

struct student{
int num;
char name[20];
char sex;
int age;
struct date birthday;
char addr[30];
}student1, student2;

结构体变量的赋值与引用

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>

struct student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

int main (){
struct student s1;
s1.num = 1;
strcpy(s1.name,"xiaoming");//不能直接用s1.name="xiaoming"这种方式。
printf("%d: %s", s1.num,s1.name);
}

数组初始化赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

struct student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};

int main (){
struct student s1 = {1,"xiaoming",'F',10,99.9,"Beijing"};
// 单引号用于字符,双引号用于字符串(字符数组)。
printf("%d: %s", s1.num,s1.name);
}

.->的区别
一般情况下用.只需要声明一个结构体,格式是结构体类型名+结构体名。然后用结构体名加.加域名就可以引用域 了。因为自动分配了结构体的内存。如同int a一样。
而用->则要声明一个结构体的指针,还要手动开辟一个该结构体的内存,然后把返回的指针给声明的结构体指针,才能用->正确引用。

结构体数组

1
2
3
4
5
6
7
8
9
10
struct student{
int mum;
char name[20];
char sex;
int age;
float score;
char addr[30];
} stu[3] = {{10101,"Tom", 'M', 18, 87.5, "Beijing Road"},
{10102,"Bob", 'M', 18, 87.5, "Beijing Road"},
{10103,"Jim", 'M', 18, 87.5, "Beijing Road"} };

结构体变量指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct student {
long num;
char name[20];
char sex;
float score;
};

void main(){
struct student stu_1;
struct student *p;
p = &stu_1;
stu_1.num = 89101;
strcpy(stu_1.name, "Li Lin");
stu_1.sex = 'M';
stu_1.score = 89.5;
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n", stu_1.num, stu_1.name, stu_1.sex, stu_1.score);
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n", (*p).num, (*p).name, (*p).sex, (*p).score);
printf("NO. :%ld\nname: %s\nsex:%c\nscore:%f\n",p->num, p->name, p->sex, p->score);
system("pause");
}

结构体数组指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#define FORMAT "%d\t%-3s\t%1c\t%2d\n"

struct student{
int num;
char name[20];
char sex;
int age;
};

struct student stu[3] = {
{1, "Tom", 'M', 18},
{2, "Kim", 'M', 19},
{3, "Jim", 'F', 20}
};

int main(){
struct student *p;
printf("No.\tname\tsex\tage\n");
for(p=stu; p<stu+3;p++)
printf(FORMAT, p->num, p->name, p->sex, p->age);
}

链表

链表是一种常见的数据结构,用于动态地进行存储分配。
以单向链表为例,它有一个头指针变量,存放一个地址,该地址指向一个元素。链表中每一个元素称为结点,每个结点都应包括两个部分,一为用户需要用的实际数据,二为下一个结点的地址。可以看出,头指针head指向第一个元素,第一个元素又指向第二个元素,直到最后一个元素,最后一个元素不再指向其他元素,称之为表尾,它的地址部分放一个NULL(表示空地址),链表到此结束。

单向链表示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

struct student{
long num;
float score;
struct student *next;
};

void main(){
struct student s1, s2, s3, *head, *p;
s1.num = 1; s1.score = 89.5;
s2.num = 2; s2.score = 90;
s3.num = 3; s3.score = 85;
head = &s1; //将结点 a 的起始地址赋给头指针 head
s1.next = &s2; //将结点 b 的起始地址赋给 a 结点的 next 成员
s2.next = &s3;
s3.next = NULL; // c 结点的 next 成员不存放其他结点地址
p = head;//使 p 指针指向 a 结点
do{
printf("%ld %5.1f\n", p->num, p->score);// 输出 p 指向的结点的数据
p = p->next; //使 p 指向下一结点
}while(p != NULL);//输出完 c 结点后 p 的值为 NULL
}

链表类型

链表类型有单向链表双向链表循环链表等类型。

双向链表是指在单向链表的基础上,每个链表元素既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针,即front和tail,front指针指向左边结点,tail指针指向右边结点。

循环链表指的是在单向链表和双向链表的基础上,将两种链表的最后一个结点指向第一个结点从而实现循环。

Windows堆表中的“快表(快速单向链表)”就是使用了单向链表作为其数据结构,“空表(空闲双向链表)”则是一种双向链表。

参考资料

数据结构之队列、栈和链表(一)
数据结构之队列、栈和链表(二)

今晚复习C语言的指针和数组。

指针

指针:指向一个变量的地址。

声明方式:

1
2
int *p;
char *q;

C语言ptr变量名的含义:pts就是**Pointer Recod(er)**的简写

指针的使用:定义指针变量、把变量地址赋值给指针、访问指针变量指向的地址的值。

空指针:指向0x0,啥也不是。

指针的值:指针的值的类型都是一样的,长度等于一个单位地址的长度的16进制数值(32位、64位等)

指针的类型:指针指向的数据类型必须是一个有效的 C 数据类型。

例如,在32位系统中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main (){
int *p;
char *t;
printf("Length of p: %d\n", sizeof(p));
printf("Length of t: %d\n", sizeof(t));
printf("Length of *p: %d\n", sizeof(*p));
printf("Length of *t: %d\n", sizeof(*t));
}

/*
执行结果:
Length of p: 4
Length of t: 4
Length of *p: 4
Length of *t: 1
*/

指针的算数运算

指针可以进行++、--、+、-等运算。

例:指针递增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

const int MAX = 3;

int main (){
int var[] = {10, 100, 200};
int i, *ptr;

/* 指针中的数组地址 */
ptr = var;
for ( i = 0; i < MAX; i++) {

printf("存储地址:var[%d] = %x\n", i, ptr );
printf("存储值:var[%d] = %d\n", i, *ptr );

/* 移动到下一个位置 */
ptr++;
}
return 0;
}

指针数组

可以通过指针来声明数组

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main (){
const char *names[4] = { "Tom", "Bob", "Tony", "Jack" };

int i = 0;
while ( i < 4 ) {
printf("Value of names[%d] = %s\n", i, names[i] );
i++;
}
return 0;
}

指向指针的指针

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main (){
int var;
int *ptr;
int **pptr;

var = 3000;

ptr = &var; // 取var的地址作为指针ptr的值

pptr = &ptr; // 取ptr的地址作为pptr的值

printf("&var(%x) => var(%d)\n", &var, var );
printf("&ptr(%x) => ptr(%x) => *ptr(%d)\n", &ptr, ptr,*ptr );
printf("&pptr(%x) => pptr(%x) => **pptr(%d)\n", &pptr, pptr, **pptr);

return 0;
}

/*
执行结果:
&var(ffe3b11c) => var(3000)
&ptr(ffe3b118) => ptr(ffe3b11c) => *ptr(3000)
&pptr(ffe3b114) => pptr(ffe3b118) => **pptr(3000)
*/

指针作为函数参数

当指针(地址)作为函数参数时,函数可以在执行过程中改变指针所指向值:
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <time.h>

void getSeconds(unsigned long *par);

int main (){
unsigned long sec;
printf("[main] &sec(%x)=>sec(%ld): \n", &sec,sec);
getSeconds( &sec );
printf("[main] sec: %ld\n", sec );
return 0;
}

void getSeconds(unsigned long *par){
printf("[getSeconds] &par(%x)=>par(%x): \n", &par, par);
/* 获取当前的秒数 */
*par = time( NULL );
return;
}

指针作为函数返回值

指针作为函数返回值时,调用方可以直接访问这个地址:
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

int * getList();

int main (){
int * i;
int j;
printf("[main] &i(%x) => i(%x)\n", &i,i);
i = getList();
printf("[main] &i(%x) => i(%x)\n", &i,i);
//for(j=0;j<5;j++)
// printf("%d\t", i[j]);
return 0;
}

int * getList(){
static int list[5]={1,2,3,4,5};
printf("[getList] &list(%x) => list(%x)\n", &list,list);
return list;
}

/*
执行结果:
[main] &i(ff94f03c) => i(5661b25b)
[getList] &list(5661e01c) => list(5661e01c)
[main] &i(ff94f03c) => i(5661e01c)
*/

数组

一维数组

1
2
int arr[] = {1,2,3,4,5};
char list[] = {"Tom", "Tony"};

多维数组

多维数组可以通过在括号内为每行指定值来进行初始化:

1
2
3
4
5
6
7
8
9
10
int a[3][4] = {  
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};

// Or

int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

多维数组的访问:

1
int val = a[2][3]; //获取数组中第2行第3个元素

数组作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

/* 函数声明 */
double getAverage(int arr[], int size);

int main (){
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;

/* 传递一个指向数组的指针作为参数 */
avg = getAverage( balance, 5 ) ;

/* 输出返回值 */
printf( "平均值是: %f ", avg );

return 0;
}

double getAverage(int arr[], int size){
int i;
double avg;
double sum=0;

for (i = 0; i < size; ++i) {
sum += arr[i];
}

avg = sum / size;

return avg;
}

数组作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* 要生成和返回随机数的函数 */
int * getRandom(){
static int r[10];
int i;

/* 设置种子 */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i){
r[i] = rand();
printf( "r[%d] = %d\n", i, r[i]);
}

return r;
}

/* 要调用上面定义函数的主函数 */
int main (){
/* 一个指向整数的指针 */
int *p;
int i;

p = getRandom();
for ( i = 0; i < 10; i++ ) {
printf( "*(p + %d) : %d\n", i, *(p + i));
}

return 0;
}

指针和数组的关系

在C语言中,数组名是一个指向数组中第一个元素的常量指针,例如

1
double balance[50];

这个数组中,balance是一个指向&balance[0] 的指针,即数组 balance 的第一个元素的地址。

因此,把 p 赋值为 balance 的第一个元素的地址:

1
2
3
4
double *p;
double balance[10];

p = balance;

使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

把第一个元素的地址存储在 p 中,就可以使用 *p、*(p+1)、*(p+2) 等来访问数组元素(也等同于*list+1)。

今晚还是在研究相对轻松的内容。主要涉及C语言的函数、作用域、预处理和头文件。

函数的声明

C语言中函数的定义:

1
2
3
4
return_type function_name( parameter list )
{
body of the function
}

C语言中函数的传值调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

void swap(int x, int y)
{
int temp;

temp = x;
x = y;
y = temp;

return;
}
int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;

printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );

swap(a, b);

printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );

return 0;
}

C语言函数的引用调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

void swap(int *x, int *y)
{
int temp;
temp = *x; /* 保存地址 x 的值 */
*x = *y; /* 把 y 赋值给 x */
*y = temp; /* 把 temp 赋值给 y */

return;
}

int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;

printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );

/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
swap(&a, &b);

printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );

return 0;
}

C语言可变参数的函数:

在C语言标准库中,stdarg.h头文件用于支持可变参数的函数,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:
定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
使用宏 va_end 来清理赋予 va_list 变量的内存。

具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdarg.h>

//一个求平均数的函数
double average(int num,...){
va_list valist;
double sum = 0.0;
int i;

/* 为 num 个参数初始化 valist */
va_start(valist, num);

/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++){
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);

return sum/num;
}

int main(){
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

函数的作用域

  1. 在函数或块内部声明的是的局部变量
    局部变量只能在函数内部使用。

  2. 在所有函数外部声明的是全局变量
    全局变量即可在函数内部使用,也可在函数外部使用。全局变量通常声明在程序的顶部,其在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。

全局变量与局部变量在内存中的区别:

全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>  
#include <stdlib.h>
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main(){
static int m1 = 2, m2;
int i = 1;
char*p;
char str[10] = "hello";
char*q = "hello";
p = (char *)malloc(100);
free(p);
printf("栈区-变量地址 i:%p\n", &i);
printf("栈区-变量地址 p:%p\n", &p);
printf("栈区-变量地址 str:%p\n", str);
printf("栈区-变量地址 q:%p\n", &q);
printf("堆区地址-动态申请:%p\n", p);
printf("全局外部有初值 k1:%p\n", &k1);
printf(" 外部无初值 k2:%p\n", &k2);
printf("静态外部有初值 k3:%p\n", &k3);
printf(" 外静无初值 k4:%p\n", &k4);
printf(" 内静态有初值 m1:%p\n", &m1);
printf(" 内静态无初值 m2:%p\n", &m2);
printf(" 文字常量地址:%p, %s\n", q, q);
printf(" 程序区地址:%p\n", &main);
return 0;
}

/*
执行结果:
栈区-变量地址 i:0xff9f9e8c
栈区-变量地址 p:0xff9f9e88
栈区-变量地址 str:0xff9f9e7e
栈区-变量地址 q:0xff9f9e78
堆区地址-动态申请:0x574ef160
全局外部有初值 k1:0x56644024
外部无初值 k2:0x5664403c
静态外部有初值 k3:0x56644028
外静无初值 k4:0x56644034
内静态有初值 m1:0x5664402c
内静态无初值 m2:0x56644038
文字常量地址:0x56642008, hello
程序区地址:0x566411b9
*/

  1. C语言的形参与实参

形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。

形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。

形参和实参的区别和联系

  1. 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

  2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。

  3. 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。

预处理和头文件

C语言的预处理器

C 预处理器相当于一个文本替换工具,会指示编译器在实际编译之前完成所需的预处理。C 预处理器(C Preprocessor)简写为 CPP。
所有的C预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
下面列出了所有重要的预处理器指令:

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

一些样例:

1
2
3
//这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。
#undef FILE_SIZE
#define FILE_SIZE 35
1
2
3
4
//这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif
1
2
3
4
5
6
7
#ifdef DEBUG
/* Your debugging statements here */
#endif
/*
这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。
它定义了 DEBUG,可在编译期间随时开启或关闭调试。
*/

预定义宏

ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

C语言头文件
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

只引用一次头文件

1
2
3
4
5
6
#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。

例如,需要指定在不同的操作系统上使用的配置参数:

1
2
3
4
5
6
7
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif

吃完饭,洗过碗,开始第一天的活动。

今天的复习内容最轻松,因为是基础中的基础。主要涉及C语言的编译器、语法关键字、运算符、变量及数据类型。

编译器

C语言在Linux上的编译器:GCC

编译tips:使用GCC在64位电脑上编译32位程序

1
2
3
4
5
6
gcc -m32 -o test test.c

#先要安装build-essential module-assistant gcc-multilib g++-multilib

apt-get install build-essential module-assistant gcc-multilib g++-multilib

C语言在Windows上的编译器:CL.EXE
Windows上的集成开发环境,通常是Visual Studio,目前最新的已经是VS 2019版本了,与早些年不同的是,VS Community版本目前对个人是免费的。
但是因为一直以来的习惯,我还是习惯用VC6.0作为我的Windows C语言开发环境。编译连接仍然采用CL.EXE和LINK.EXE,以及NMAKE.EXE。

基本的编译语法

以Hello World为例,代码如下:

1
2
3
4
5
6

int main(){
printf("Hello World\n");
return 0;
}

Linux(Debian)下的编译过程
gcc -o helloworld helloworld.c

Windows下使用CL.EXE进行编译

1
2
cl.exe /c helloworld.c
link helloworld.obj /LIBPATH:C:\Tools\Binary\VC++6.0\VC98\Lib\

这里C:\Tools\Binary\VC++6.0\VC98\Lib\是我的VC6路径,并且执行cl和link需要将程序路径加入到环境变量。

语法关键字

1
2
3
4
5
6
7
auto  break case  char  const  
continue default do double else
enum extern float for goto
if int long register return
short signed sizeof static struct
switch typedef unsigned union void
volatile while

部分关键字含义

  • continue 结束当前循环,开始下一轮循环
  • extern 声明变量或函数是在其它文件或本文件的其他位置定义
  • goto 无条件跳转语句
  • register 声明寄存器变量
  • typedef 用以给数据类型取别名
  • volatile 说明变量在程序执行中可被隐含地改变

运算符

基本运算符

加减乘除、取模、自增、自减: +,-,*,/,%,++,–

赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
=:简单的赋值运算符,把右边操作数的值赋给左边操作数    C = A + B 将把 A + B 的值赋给 C
+=:加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-=:减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*=:乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/=:除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%=:求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<=:左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>=:右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&=:按位与且赋值运算符 C &= 2 等同于 C = C & 2
^=:按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|=:按位或且赋值运算符 C |= 2 等同于 C = C | 2

逻辑运算符

1
与或非:&&,||,!

位运算符

1
与、或、异或、取反、左移、右移:&,|,^,~,<<,>>

杂项运算符

1
2
3
&    返回变量的地址
* 指向一个变量
? : 条件表达式三元运算符

变量及数据类型

基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
整数类型:
char 1 字节 -128 到 127 或 0 到 255
unsigned char 1 字节 0 到 255
signed char 1 字节 -128 到 127
int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
short 2 字节 -32,768 到 32,767
unsigned short 2 字节 0 到 65,535
long 4 字节 -2,147,483,648 到 2,147,483,647
unsigned long 4 字节 0 到 4,294,967,295

浮点类型:
float 4 字节 1.2E-38 到 3.4E+38 6 位小数
double 8 字节 2.3E-308 到 1.7E+308 15 位小数
long double 16 字节 3.4E-4932 到 1.1E+4932 19 位小数

枚举类型

可以在定义枚举类型时改变枚举元素的值:

enum season {spring, summer=3, autumn, winter};

没有指定值的枚举元素,其值为前一元素加1。此例中spring的值为0,summer的值为 3,autumn 的值为4,winter 的值为5。

三种写法

  1. 先定义枚举类型,再定义枚举变量
1
2
3
4
5
6
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;

  1. 定义枚举类型的同时定义枚举变量
1
2
3
4
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
  1. 省略枚举名称,直接定义枚举变量
1
2
3
4
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

引用枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

enum DAY {
MON=1, TUE, WED, THU, FRI, SAT, SUN
};

int main() {
enum DAY day;
day = WED;
printf("%d",day);
return 0;
}

应该找点让自己开心的事情做。

C语言是我进入计算机领域学习的第一门语言。可惜工作这么多年,早已忘的支离破碎,对链表、数据结构乃至系统编程的知识,早已经还给老师了。这对今后的学习是非常不利的。所以计划从今天开始,抽空花30个晚上,每晚花1~2个小时的时间复习C语言,尽量保证连续。

我的复习会比较跳跃,这个系列的文章,也仅适合作为个人的技术笔记,不太适合作为入门资料进行参考。

我可能会在文章中提及一些调试、算法以及工程方面的技术,相信其中有些内容对搜索引擎来说会是一个比较好的长尾关键词,这样也给一些编程过程中遇到问题的朋友一些参考。本着少废话的原则,尽量避免探讨过于简单的技术。否则可能会沦为小白记事本,反而容易误导网友。

暂时给自己拟定了以下复习计划,30个夜晚主要这样安排(后续可能根据实际情况有所调整):

第一夜:编译器、语法关键字、运算符、变量及数据类型
第二夜:函数、作用域、预处理和头文件
第三夜:指针和数组
第四夜:存储类型、结构体、链表
第五夜:C语言标准库、错误处理和调试、输入输出
第六夜:内存管理、读写文件、管道
第七夜:线程、多线程
第八夜:进程、多进程
第九夜:C语言常见的Linux系统调用
第十夜:C语言常见的Windows系统调用
第十一夜:在C语言中使用内联汇编
第十二夜:使用C语言探索堆栈
第十三夜:网络编程1-网络协议及其数据结构
第十四夜:网络编程2-Socket通信
第十五夜:网络编程3-常见的应用层协议
第十六夜:编写一个Linux的服务
第十七夜:编写一个Windows的服务
第十八夜:C语言动态链接与静态链接
第十九夜:编写一个Linux用户态Trojan
第二十夜:编写一个Windows用户态Trojan
第二十一夜:Linux内核编程1-内核源代码分析
第二十二夜:Linux内核编程2-实现一个内核模块
第二十三夜:Linux内核编程3-实现一个块设备驱动
第二十四夜:Linux内核编程4-实现一个字符设备驱动
第二十五夜:Linux内核编程5-实现一个内核Trojan
第二十六夜:Linux内核编程5-在Linux内核中瞎搞
第二十七夜:Windows内核编程1-Windows内核初探
第二十八夜:Windows内核编程2-Windows驱动编程研究
第二十九夜:Windows内核编程3-在Windows内核中瞎搞
第三十夜:学习总结

真是一个宏大的计划,甚至不知道自己能不能完成,有点期待。

希望可以自律、坚持地完成下去。

参考资料:

Linux kernel下的内核socket编程
Linux系统编程

在看过新海诚的《君の名は。》之后,感动的同时,非常喜悦。
我曾在千与千寻这篇文章中,写过宫崎骏成长的环境,简单表明了,时代造就大师这一观点。

新海诚,就是让人感到他是日本漫画在宫崎骏之后又一位大师,为这个世界带来满满诚意。

我说过我们老了以后不会无聊,事实上在我们还如此年轻的时候,就已经不无聊了。

在豆瓣看了这样一篇关于天气之子的影评,自觉表达功力粗浅,看到这样字字珠玑的影评,满脑子只有刘关张结拜时张飞的那句”俺也一样“,带着一种热泪盈眶。

文艺评论并不是我的特长,每次写所谓的影评,我常常容易看起来跑题,不怎么提及影片本身,而更多的出离作品,结合我自己对生活和艺术的主观感受,去品味作品的灵魂。

日本比我们更早进入繁荣时期,曾经的日本年轻人比我们更早地遭遇到经济发展、社会进步所伴随的迷茫与困惑。他们的如今,折射出我们的今后。

我一直将日本这个国家视作一个有趣的观察对象。他们和我们拥有同源的东亚文化,可以追溯到盛唐时期,甚至在某些方面,比我们保存的更完好,例如建筑、礼仪、料理等等。在亚洲的民族当中,日本一直是一个学习能力很强的民族。在古代,他们向中国学习先进的文化,并良好地继承和发扬。在近代,他们从欧美学习机械和电子,并在20世纪末创造了空前的经济辉煌。这无疑是亚洲班里一位踏实的尖子生。

为什么观察日本?可以认为日本就是东亚文明升级过程的一个小型实验场。在这个实验场里可以看到东西方文化的交融、碰撞,传统文明与现代文明的对立、统一,而这些现象,放在我们的社会同样适用。社会在发展过程中,很多事物当然应该以史为鉴。而如果有一个能够把玩和观测的经验模型,岂不更妙?

我们这代人此时所遭遇的困惑和际遇,也正是日本上一个阶段的年轻人所经历过的。他们也经历过科技的进步、房市的繁荣、股市的爆发,乃至美国的打压。蜡笔小新五岁的时候,家里房贷还剩下30年,美芽喜欢逛商场,广志带回家一台笔记本电脑,一家人偶尔旅行和野餐。这些小民生活的方方面面,如今只不过是换了时间和形态,重演在我们的人生历程中。他们的社会就像是时空隧道传出的一道剪影,让我们提前预知一些事物的发展方向。事实上,由于世界局势发展的不确定性,能透过他们预知的距离也越来越短。我们正在一边观测,一边超越。今天东京街头的年轻人,可能和首尔、洛杉矶、上海的年轻人的思想差不了太多。

漫画有时是对生活的解构和期望。很多在现实中难以如愿的,就去漫画里圆满、去发泄。

看新海诚的《天气之子》,最多的感触是,社会角落中生活的人们,他们眼中的天空,是怎样的耐人寻味。

他们的快乐和痛苦,他们的青春和爱情。他们那些刻骨铭心的记忆,他们那些难以割舍的爱人。

社会的条条框框,约束人生的方方面面,将棱角分明的少年,磨成世故圆滑的大人。

习惯了资本的游戏,舍弃了纯粹和天真。既抱怨着透不过气的生活,却又践踏着自由的土壤。

厌恶荒诞戏谑的现实,又更卖力地表演着丛林法则。没有放弃一切的勇气,却还觊觎灿烂美好的晴天。

这解释了为什么,年轻的晴女拥有了治愈东京的神力,也愿意抛弃,只选择和所爱的人在一起,守望朝夕。

新开辟了一个博文分类,社会
分类叫做社会,但是却不怎么敢聊社会。至少有些话题我是不敢聊的。
但是今天,还是来随便聊两句,作为这个系列的开篇(不保证有后续)。

从某厂“不让”注销说起

如果你是一位这厂用户并且你能幸运地找到以上页面,你也许会有知乎上这个问题一样的困扰。

为什么大部分的中国互联网公司,都把注销功能做的不尽如人意甚至压根不做呢?这个问题实在太深刻,深刻到令我无从解读。

其实根据工信部《电信和互联网用户个人信息保护规定》第九条,用户完全有注销帐号的权利。但很多互联网公司都没有为用户提供这项权利。

第九条 未经用户同意,电信业务经营者、互联网信息服务提供者不得收集、使用用户个人信息。

  电信业务经营者、互联网信息服务提供者收集、使用用户个人信息的,应当明确告知用户收集、使用信息的目的、方式和范围,查询、更正信息的渠道以及拒绝提供信息的后果等事项。

  电信业务经营者、互联网信息服务提供者不得收集其提供服务所必需以外的用户个人信息或者将信息用于提供服务之外的目的,不得以欺骗、误导或者强迫等方式或者违反法律、行政法规以及双方的约定收集、使用信息。

  电信业务经营者、互联网信息服务提供者在用户终止使用电信服务或者互联网信息服务后,应当停止对用户个人信息的收集和使用,并为用户提供注销号码或者账号的服务。

  法律、行政法规对本条第一款至第四款规定的情形另有规定的,从其规定。

也有一些公司为用户提供了“注销”这项功能,但注销删除是有区别的,我相信用户选择注销帐号,更多的是希望能够从数据库中将自己的信息删除。然而从程序员的角度来看,对数据库执行一个删除操作,显然要比增加一个删除标记字段的风险大的多。尤其是对于现在靠用户数据卖钱的公司来说,删除用户,那就是在割自己田里的韭菜苗,挖韭菜根啊。何况,工信部的规定,也并没有提到要删除,只说需要提供注销的功能。你要注销,可以,把你的账号锁定,手机号释放,禁止你登录,标记为注销。

更何况,现如今很多公司都提倡什么微服务、数据中台,本来一个MySQL就能搞定的数据,非要在Redis里放一点,Mongo里面放一点,ES里面放一点,这就导致如果要真的从系统中完整删除一个帐号,远比想象的要复杂,数据和业务之间可能会有错综复杂的裙带关系。你要问这些公司为啥要这样做?很简单,员工要吃饭,上司要成绩。不上新架构新业务,经费怎么花?奖金谁来拿?从老板层面而言,就更是如此。养着一帮程序员,工资又那么高,不让他们做点事情压榨压榨,自己岂不是亏了。可是业务是固定的,用户也是稳定的,让他们做啥好呢?那就什么新做什么吧,这阵子大数据新,就上大数据,过阵子AI火,那就用AI,区块链概念好,那就玩区块链……

互联网圈里很多人有个臭毛病,就是代码也许写不了几行,但是吹起NB绝对是最靓的仔。

互联网公司个个看起来高大上,可是大部分不过是CRUD功能堆砌起来的信息垃圾堆而已。如果不清楚什么是CRUD,就这么跟你说吧,CRUD说白了就是基础的增删查改(Create,Retrieve,Update,Delete)。

以SNS网站为例,其基础功能从逻辑本质上来看,无非就是信息创建、信息修改、信息索引,就是增删查改。并没有什么高大上,反倒是业务庞大了以后,雇了太多活儿糙的程序员,导致部门臃肿,架构复杂,业务更加垃圾。

说到这里,有些代码孔乙己可能会出来杠。算法和架构的事,能叫CRUD吗?紧接着,便是分布式缓存、后端渲染、大数据、机器学习……写字间里又传来一阵快活的笑声。

我根本不想杠,写博客本来就图个乐。

不过话说回来,也不能小瞧这些创造垃圾的公司,他们割起韭菜可是很有水平的。不信你看,中文互联网公司,盈利的高招儿,我来悉数一下:

做文档的公司,放贷款;
做手机的公司,放贷款;
做社交的公司,放贷款;
做视频的公司,放贷款;
做搜索的公司,做医疗;
以上公司,也偶尔插播植发广告。

美名其曰运营。实际上是杀猪。在他们眼里,用户就是猪,也许有的时候,用户还不如猪。

聊聊互联网自由

前面说到互联网公司的问题,不得不进一步思考。互联网发展至今,是否还有人记得它的初心?互联网究竟是解放了人类,还是为人类制造了新的枷锁?

互联网最开始,人们希望的是拥有自由、开放、共享的互联网,让人们可以在浩瀚的数字海洋里畅快地傲游。可如今,TikTok被威胁,让人甚至感觉到一种魔幻。曾经我们以为大洋彼岸的那块土壤,可以繁衍出自由的互联网。可如今再看,所谓自由不过也是游戏规则制定者嘴里的说辞罢了。

放眼整个互联网世界,如今似乎正在经历一场浩劫。欧洲、俄罗斯,都在考虑封锁自己国家的互联网。真不知道今后,还有没有能够让全人类互相连接甚至连接一切的互联网。想起圣经里面提到的,上帝为了阻止人类建造通往天堂的巴别塔,把人类分散在世界各地,让他们说不同的语言,从而无法协作。而曾经人们一度相信互联网可以让全人类产生紧密的联系,可以促进一切的发展。然而事实上,互联网却只沦为了少部分人的舆论场,投机分子的势利场,资本家们的养猪厂,犯罪的温床。

虽然也有过奇妙的一幕,就如维基解密棱镜门、巴拉拉文件、各种门。微博也曾经为寻人、破案、反腐带来了一些不可思议的效果。

可互联网世界更多的却还是充斥着一种难以名状的混沌。

这一切因果,究竟是互联网的罪恶基因在作怪,还是人类的秉性使然?

是否自由就意味着混乱?是否以人类的本性,根本就不配拥有互联网这样的观照之境来普渡众生。

什么时候互联网的世界,才可以有一片纯净的天空。人类可以利用互联网,传播真正有价值的信息和言论,透明而严谨的真相,而不是140个字的废话,声色犬马。

也许需要先提高全体人类自身的认知水平,才能逐渐实现这一切。也许,没等到提高全体人类的认知水平,这一切就已经毁灭在黎明前夕。

总之,今天我已经选择注销大部分社交帐号,选择在浩瀚的数字海洋里,自闭成为一座孤岛,就像这个网站一样。

选择阅读更多纸质书籍,投入于更多经典,沉下心来,去踏实研究这世间的学问。

我相信,即使是一艘小船,也可以纵横四海,阅尽千帆。

海船

2020/07/26摄于 南京-大胜关

很久没有写字了,但却从未停止思考。这么一方小天地,自是无人问津,可以畅快表达。

永恒的事物,必要时间去证明它的价值 – 或是它的荒诞。

思考太多却很少见诸笔端的主要原因有三:

一是不够自信,一直处于自我怀疑之中;
二是读书太少,更加不敢开口,怕暴露智商;
三是不敢胡说,文字表达天生存在缺陷,开口即是错。

在我自己开辟的一方赛博领土之上,我倒是愿意说一些莫名其妙的真话。但最好也是猥琐到让人不能轻易看懂,自己也安全。

今天就来谈谈,很久以来,一些一直在思考却从未有结果的事情。我想很多我早已想通的道理,而世界却未必明白。

所以我将它写下来,孤独的思想者,把一切苍白的辩驳都交给时间。

眼前的光景,是荒唐的人间。世界观没来得及稳固就被颠覆的感觉,不知道是否有人能够体会。

我们的生活,B站、抖音,歌舞升平,一浪高过一浪。生活仿佛从未发生变化甚至快要高潮一样,而这正是当下最该警惕的地方。

不禁想回忆起,从什么时候开始觉得生活变得逐渐不一样?我觉得是从2012年。

混沌中心

混沌是什么?混沌是无法描述的,就如此刻我想诉说的事物,它无法被琢磨,无法被找寻,无法被把握。

但能清楚的感受到它的存在。更进一步的,是能感知到,它将去向哪儿。

我们身处混沌的中心,眼前充斥着一切迷雾,混沌假以掩饰,令我们亦步亦趋。

我们的生活,如今被互联网所支配。而互联网是一个无法停止的事物,一旦启动,就必将改变眼前的一切。

而有一些价值观,需要被表达,也是此文的意义。

可能有些人,看到这里,就不再继续读下去了。这也正是每当想要对这个世界阐述一种立场时,最悲观的地方。

黑客精神

黑客在看待一个问题时,首先是质疑。如果想要知道一道上锁的门是否可靠,只需要推敲即可。记住,质疑,再然后是推敲。

世上之事,大抵如此。从SQL注入,到人生的意义。其实世上之事,大都经不起推敲。

地球上所有的信息系统,都有漏洞。甚至可能连数学理论本身都有漏洞。如果你认为printf(“hello world\n”)这样的代码不会有漏洞,请看看射线导致磁盘、内存位反转的案例。类似这样的,甚至更加古怪的问题,人类黑客,可能永远也无法发现它。而很多时候逻辑深处的漏洞,甚至比物理漏洞还要隐蔽。之所以常人觉察不到,仅仅是因为认知和格局还无法辅佐我们抵达。

可悲的是,如今大部分具有黑客思维的人,所从事的工作,竟然是在给这个世界上最愚蠢的程序猿们开发的CRUD系统寻找所谓的安全问题!更可悲的是,很多在质疑和推敲方面具有良好潜质、良好的思辨能力的选手,竟沉迷在这样的工作当中无法自拔,自我满足。

黑客生存的意义,绝不应该局限在给信息系统寻找漏洞这件经不起推敲的事情。当好不容易具备了与别人不一样的怀疑视角,一定不要把这种能力埋没在和傻瓜比数数,以及指导他们做作业这些无聊事情上。而是应该去研究金融、经济、Politics、管理。当然,如果你的脑核足够富余,哲学和数学更是个不错的选择。

只提出愿景,而忽视现实,是非常惹人讨厌的。你可能会说,黑客也是人,黑客要吃饭,养家糊口,就必须去做一些很傻的事情。其实这是经不起推敲的,看透一切事物的本质,便知道物欲也是虚无。穷且益坚,求真务实,就成为了一个黑客该有的​追求。

其实,只要能理解,这个世界上,大部分事情,都经不起推敲的​道理,就不会再去追求物质。

​​就说金钱吧。在以黑客思维去看待地球文明时,货币系统只各个互斥的群体为了内部耦合与外部协作而构建出的一套​算法罢了。货币、贵金属、古董,乃至一切等价物,都只是对​价值结构体的引用罢了。

个体的贫穷和富有,取决于这套算法的​实现逻辑。掌握了这套算法,未必能实现富裕。有commit的权限和编码的水平,兴许倒可以实现利益最大化。

但是跳脱出这个系统,才是黑客精神。仅仅沉迷在这套系统之间,还是​没有意义。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×