solomonxie / blog-in-the-issues

A personalised tech-blog, notebook, diary, presentation and introduction.
https://solomonxie.github.io
67 stars 12 forks source link

C | C++ Learning Curve #34

Open solomonxie opened 6 years ago

solomonxie commented 5 years ago

❖ C Intro

C语言的代码运行速度相当于汇编语言的运行速度,所以一般用C语言作为系统级开发语言。

环境搭建

C语言只需要一个编译器C Compiler就可以工作,代码直接用各种文本编辑器写就行了。 目前比较流行的C编译器有:

gcc -v检查当前编译器版本,如果没有,则需要安装gcc了。

Ubuntu上,

sudo apt-get install build-essential

Mac上,需要在App Store安装Xcode,然后就有了gcc。

Windows上,需要安装MinGW

编译和运行C代码

假设现在写了一个最最最简单的hello-world.c,如下:

#include <stdio.h>
int main() {
    printf("Hello, world!");
    return 0;
}

注意:C语言中的#不代表注释,而是要调用预处理器的意思。

最最简单的编译命令就是:

$ gcc hello-world.c

然后目录中就会自动生成一个a.out的文件,是个二进制机器可读懂的东西。这个名字a.out是默认的,因为我们没有自己指定名称。

如果要指定输出名称(-o 参数):

$ gcc hello-world.c -o hello

这时候目录就会出现一个叫hello的没有后缀名的文件。

运行: 因为C代码编译出来是二进制可执行文件,也就是文件默认就有x属性,所以我们可以像bash脚本一样直接在命令行打开:

$ ./hello

这样就会显示出Hello, world!%这样的信息。

C的数据类型

C的数据类型分为几大类,各个类下有非常多的细分类型。 与动态类型的Python等语言不同,作为静态类型的C语言必须严格区分类型,这样才能在创建变量时候就给它开辟一块固定的内存空间。

C的数据类型分为这几大类:

其中,整数类型可以细分为以下: image

可以看到,同样的细分类型,如char,在每种OS和每种CPU架构上分配的内存空间是很不同的。这也就是为什么程序在另一个平台不那么容易直接用的原因。

所以为了确定当前OS和架构环境下的类型所占内存,需要用sizeof(type)方法来检验。

变量类型

C变量分为如下类型:

image

C语言声明变量:

// 只声明一个变量的类型(只分配空间,不赋值)
int i;
// 为变量赋值
i = 10;

// 或,声明变量类型,且初始化赋值
int i = 123;

定义常量,有两种方法:

// 使用预处理器
#define LENGTH 10
#define WIDTH 5

int area = LENGTH * WIDTH;

// 或者,
// 使用const关键字
const int HEIGHT = 100

int volume = area * HEIGHT;

存储类

C中的存储类可以定义变量或函数的生命周期活动范围,放在type前作为修饰。 一般分为:

条件判断

if...else

单句if...else:

int a = 100;

if( a > 20 )
    printf("a 小于 20\n" );
else
    printf("a 大于等于 20\n" );

多句:

int a = 100;

/* 检查布尔条件 */
if( a = 20 ) {
    printf("a = 20\n" );
} else if(a = 10) {
    printf("a = 10\n");
} else {
    printf("a 大于 20\n" );
}

printf("a 的值是 %d\n", a);

switch

char grade = 'B';

switch(grade) {
   case 'A' :
      printf("很棒!\n" );
      break;
   case 'B' :
   case 'C' :
      printf("做得好\n" );
      break;
   default :
      printf("无效的成绩\n" );
}

printf("您的成绩是 %c\n", grade );

三元运算符

Condition ? Exp_if_True : Exp_if_False;

循环

循环分为:

循环控制语句有:

for循环

for( int a = 10; a < 20; a = a + 1 ) {
   printf("a 的值: %d\n", a);
}

无限for循环:

for( ; ; ) {
   printf("该循环会永远执行下去!直到Ctrl+C终止程序。\n");
}

while循环

while( a < 20 ) {
   printf("a 的值: %d\n", a);
   a++;
}

无限while循环:

while( 1 == 1 ) {
   printf("该循环会永远执行下去!直到Ctrl+C终止程序。\n");
}

do...while循环

do {
   printf("a 的值: %d\n", a);
   a = a + 1;
} while( a < 20 );

使用函数

不同于高级语言,C语言的函数分为声明定义,函数头相当于要写“两遍”。 函数的定义和声明都可以在任意位置写, 但是必须在正式使用前写其中一个,否则编译器会找不到引用。

示例:

#include <stdio.h>

int main() {
    // 先要声明函数:
    int max(int num1, int num2);

    // 才能使用函数
    printf( "%d", max(3, 90) );
}

// 定义函数
int max(int num1, int num2) {
   int result;

   if (num1 > num2)
      result = num1;
   else
      result = num2;

   return result; 
}

以上为最简单的函数调用,且传输的参数值3 & 90,会被复制到函数中使用。函数中无论怎么改,都不影响原先的变量值。

但是,如果想要允许函数修改原变量的值,那么就不能直接传递值,而是要传递引用。即把每个变量的内存地址的指针传进去:

#include <stdio.h>

void clear(int *a, int *b);

void clear(int *a, int *b){
    *a = 0;
    *b = 0;
    return;
}

int main(){
    int a = 10;
    int b = 20;

    clear(&a, &b);

    printf( "a = %d\n", a);
    printf( "b = %d\n", b);
    return 0;
}

注意,其中函数参数声明时候是在变量前加*,而使用时,传入变量引用需要在变量前加&

局部变量和全局变量

和其它语言一样。

但是要注意: 当局部变量被定义时,系统不会对其初始化,必须手动去初始化。 定义全局变量时,系统会自动对其初始化。

形参与实参

C的函数的参数有很多种类型:

// 参数为指定大小的数组
void myFunction(int param[10]) { /*....代码....*/ }

// 参数为未指定大小的数组
void myFunction(int param[]) { /*....代码....*/ }

// 参数为指针的数组
void myFunction(int *param) { /*....代码....*/ }

函数返回数组

C语言不支持直接返回数组,但是可以返回一个指针,指向数组。

int * myFunc() {
    arr = {1,2,3};
    return arr;
}

int *p;
p = myFunc();
solomonxie commented 5 years ago

❖ C语言数据类型 [DRAFT]

数组类型

格式:

type arrayName [ arraySize ];

示例:

// 声明数组 (只创建指定大小的内存空间,不赋值)
double balance[5];

// 数组初始化并赋值 (注意是{} 符号)
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

// 或者,自动识别数组中项目数量
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

// 为数组的某一项赋值
balance[4] = 50.0;

// 读取数组某一项元素
double salary = balance[2];

多维数组:

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

// 或者,省略{}括号,根据纬度自动填充
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

// 访问多维数组
int val = a[2][3];

字符串

C语言中,字符串实际上是一个特殊的数组,即每一个字符为数组中的一个元素,且数组的最后一个元素必须是'\0'

如以下二者是完全想等的:

// 按数组形式直接声明字符串:
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// 更简单的方式声明字符串:
char greeting[] = "Hello";

那么,无论用以上哪种方式创建一个数组,内存中的结构都是一样的:

image

枚举类型 Enumeration

理解枚举类型: C中的Enumeration是一个常量集合,相当于Python的Dict字典变量,即key-value数组。

在Python中,定义一个字典是:

WEEK = {
    "MON": 1,
    "TUE": 2,
    "WED": 3 
    #....
}

而在C中,定义一个“字典”是:

enum WEEK {
    MON = 1,
    TUE = 2,
    WED, THU, FRI, SAT, SUN
}

C中的“字典”特殊的是,如果后面的keys不赋值,那么会自动在前面一个值上+1。如果前面没有项,那么自动变为0。

C语言中,定义枚举类型变量,需要两步:

  1. 自定义枚举类型 (相当于创建类)
  2. 定义枚举变量 (相当于根据类创建对象实例)
// 先定义类型
enum WEEK{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
// 再定义变量
enum WEEK day;

// 或者,同时定义类型和变量
enum WEEK {
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

// 或者,省略类型名称,直接定义变量(只能用一次)
enum {
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

使用enumeration:

enum WEEK day;
day = TUE;

printf( "%d", day)

定义的时候好理解,使用的时候这里非常容易混淆。

switch判断enumeration:

// 定义枚举
enum color { red=1, green, blue };
enum  color favorite;

fav = blue

// 开始判断
switch (fav) {
   case red:
     printf("你喜欢的颜色是红色");
     break;
   case green:
     printf("你喜欢的颜色是绿色");
     break;
   case blue:
     printf("你喜欢的颜色是蓝色");
     break;
   default:
     printf("你没有选择你喜欢的颜色");
}

循环遍历enumeration类型(前提:枚举中定义的数是连续的):

// 定义枚举
enum DAY {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

// 循环遍历枚举
for (day = MON; day <= SUN; day++) {
   printf("枚举元素:%d \n", day);
}

结构体

C语言中的结构体实际上是的原型,是可以让用户自定义的数据结构。

定义一个结构的语法,例如:

struct Books {
   char  title[50];
   char  author[50];
   char  subject[100];
   int   id;
};

使用结构体:

struct Books b = {"C 语言", "RUNOOB", "编程语言", 123456};

printf("%s, %s, %s, %d", b.title, b.author, b.subject, b.id);

作为函数参数的结构体:

void func(struct Books b) {
    b.title = "C语言";
    b.author = "RUNOOB";
    b.subject = "编程语言";
    b.id = 123456;
}

共用体

和结构体相似,是让用户自定义的数据类型,只是允许在相同的内存位置存储不同的数据类型。

定义语法:

union Data {
   int i;
   float f;
   char  str[20];
};

使用共用体:

union Data d;  

d.i = 10;
d.f = 220.5;
strcpy( d.str, "C Programming" );
solomonxie commented 5 years ago

❖ C指针 Pointer [DRAFT]

作为系统底层语言,经常需要操作内存等,如动态内存分配,这是没有指针就无法完成的。

理解 &*

每个变量都有两部分:⓵数据值 ⓶内存地址 而指针相当与一个反变量,它也是两部分但是相反: ⓵内存地址 ⓶数据值

假设有一个变量int var = 100,和一个指针int *ptr = &var。 当我们把指针指向了那个变量的时候,实际上的操作只是:把⓵和⓶换个位置。

直接调用varptr的时候,都是显示⓵部分的值。

但是如果要显示隐藏在背后的⓶部分的值,就需要用到这两个操作符号:

明白这个“互反”规则后,以后的**ptr指向指针的指针也就好理解了。

使用指针

假设现在有一个普通变量int height = 10;,那么查看这个变量的内存地址的方法就是打印&height

C指针也是一个变量,但是它的值是另一个变量的内存地址。

声明C指针型变量的方法:

type *var-name;

// 如
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

怎么给指针赋值?当然不能手动写,而是需要把另一个变量的内存地址传给它:

int variable = 1234;
int *addr;

addr = &variable;

这样就完成了指针的赋值。

怎么打印指针? 打印的时候要用特殊的%p格式化符号,代表pointer

// 打印指针中的指向的内存地址:
printf("%p", addr);

// 等同于:
printf("%p", &variable);

// 获得这块内存中的存储值:即刚才赋值的1234
printf("%d", *addr);

指向指针的指针

参考:Making Peace with Double Pointers

一个指针ptr指向variable的内存地址,那么另一个指针pptr指向前一个指针ptr,就相当于得到了variable的值。有点像互反的关系。

声明一个指针的指针:

int  var;
int  *ptr;
int  **pptr;

var = 3000;
ptr = &var;   // 获取变量的内存
pptr = &ptr;  // 获取指针的值

// 打印
printf("%d\n", var );
printf("%d\n", *ptr );
printf("%d\n", **pptr);

// 输出结果全部都是3000

当然,你还可以设计triple pointer,比如:int ***ppptr,把它指向一个double pointer: **ptr.

函数指针

// 声明函数
int max(int i, int j) { /* ... */ }

// 声明函数指针
int (* p)(int, int) = & max;   // &可以省略

// 使用函数指针
p(12, 34)

指针作为参数

// 声明普通函数
int max(int i, int j) { /* ... */ }

// 声明参数为指针的函数
void func( int *arr, int (*max)(void) ) { /* .... */ }

// 使用函数(正常用,只不过不会把参数“复制“进去,而是直接使用,相当于公用变量)
func( [1,2,3], max );

从函数返回指针

// 声明函数
void * func() { /* ... /* }

// 传递函数
int *ptr = func();

// 使用函数
ptr();

指针的算术运算

指针的算术运算有:

指针的p++p+1, p--, p-1等,都是指的指针位移。 假设一个指针指向了数组,如:

int arr[] = {123, 456, 789};
int *ptr = arr;

那么,实际上指针是指向了数组的第一个元素。 另外,由于数组本质上也是个指针,所以不需要&arr来得到内存地址。

也就说,这个时候,实际上ptr == &arr == &arr[0],这三项的值是一样的。 那么当ptr++ptr=ptr+1的时候,指针移向了下一个位置,即arr[1]的内存地址。这时:ptr == &arr[1]

同理,ptr--只是往前位移了1而已。

既然指针ptr是一个简单的位置信息,那么我们同样可以比较指针

if (ptr < &arr[2])
    printf("%d", ptr);
solomonxie commented 5 years ago

❖ C动态内存管理 [DRAFT]

理解内存管理

内存管理分static memory静态内存和dynamic memory动态内存。

一般当我们直接设置变量如int v = 99;时,就是静态内存设定。这个变量的内存保留直到程序结束。 但当我们不知道变量是什么,它会动态变化时,我们就要动态设定内存了。

只有动态内存设定时,才会用到ma-la ca-la re-a-lac and free

C语言管理内存的函数

主要是用<stdlib.h>这个头文件,其中常用的方法有:

光看名称有点迷惑,实际上:

记忆:像念绕口令一样重复念:ma-la ca-la re-a-lac and free. 参考:Allocating memory with malloc, calloc, realloc, and free

动态给变量设定内存

设定内存时,先要给指针设定,然后再用指针来储存具体的值。

最简单的内存设定方法就是malloc

// 单变量
int *v = malloc( sizeof(int) );
*v = 99;

// 长度为9的数组
int *arr = malloc( sizeof(int)*9 );
*arr[0] = 34;
*arr[101] = 99; // 这样直接写不好,很有可能超出界限

callocmalloc区别只在于,calloc有两个参数,一个是单项所占内存大小,另一个是多少个数量。 用法如下:

int *arr;
*arr = calloc( sizeof(int), 9 );

realloc用于resize,即更改内存占用大小。第一个参数是指针,第二个参数是新的内存占用大小。

*new_arr = realloc( arr, sizeof(int)*9 )

free用来清除内存。用完变量后即使清除是良好的实践。 如果不自己清除的话,很有可能造成对内存的占用累积越来越多,导致内存过载。

free( arr );
arr = NULL;

使用Valgind检测程序的内存信息

Valgind能够检测并动态显示某个二进制文件的内存占用信息,非常友好。

Mac安装:

$ brew install valgrind

Ubuntu安装:

$ sudo apt-get install valgrind

注意:Valgrind程序比较大,Ubuntu上需要150MB左右,Mac上50MB左右。

使用:

$ valgrind ./hello

注意:这里必须要用./指定当前目录,而不能直接valgrind hello

开始后,会显示类似这样的信息:

image

解读Valgrind信息 [DRAFT]

HEAP SUMMARY中,

LEAK SUMMARY中,会显示内存泄漏的问题。 如果没有泄漏,则会在definitely lostindirectly lost处显示0。

如果没问题的话,就会在最后的ERROR SUMMARY那行显示0 errors from 0 contexts

solomonxie commented 5 years ago

❖ 调试C, C++的利器:GDB

参考:Learn GDB in 60 seconds 参考:使用 GDB 调试 Linux 软件 - IBM

GDB是GNU出品的超强力debug工具,有人因为觉得是在命令行里手动调试而看不起它,但是真的很难说你用的哪个IDE调试器比这个强。

Facts:

对C/C++程序的调试,需要在编译时就加上-g选项:

$ gcc -g hello.c -o hello
$ g++ -g hello.cpp -o hello
$ clang -g hello.c -o hello

然后运行gdb来调试-g选项编译后的二进制文件:

$ gdb ./hello

进入调试后,按照顺序,一般的操作有:

Mac上的gdb及其替代品

gdb对Mac的支持不好,经常会报错:

Unable to find Mach task port for process-id 82541: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))

而且问题很难处理,所以一般情况下是找个替代品。

cgbd

cgdb本身只是gdb的包装,让gdb更好看更好用。

Mac安装很简单:brew install cgdb Ubuntu安装也一样:sudo apt-get install cgdb

image

配置很好看,默认自动分屏,用颜色显示。 但是,既然只是gdb的包装,所以gdb在Mac上的please check gdb is codesigned - see taskgated(8)还是不能解决。

LLDB:在Mac上调试的正确用法

由于GDB在Mac上的一系列问题,lldb属于Xcode默认调试器,无需任何配置。 唯一的问题就是,调试中的各个命令稍有不同。但是不用担心,学起来非常快。

参考:The LLDB Debugger 参考:GDB and LLDB Command Examples

常用命令(GDB与LLDB对比):

image

与gdb相比,目前已知缺点:很多IDE、GUI、插件都是基于gdb的,所以LLDB就与他们都无缘了。

solomonxie commented 5 years ago

C语言的文件读写 [DRAFT]

fopen 最简单读写

简单读文件:

FILE *to_read = fopen("test.txt", "r")

if (to_read == NULL) {
    printf("Cannot open file\n");
    return -1
}

char c;
while ( (c=fgetc(to_read)) != EOF) {
    printf("%c", c);
}
fclose( to_read );

简单写文件:

open读写

参考视频:Reading and Writing Files in C, two ways (fopen vs. open)

注意一:

注意二:openfopen慢100倍。 为什么?为什么即使fopen是调用open读写文件的,还要比open慢呢? 是因为fopen调用了缓存,即Buffered I/O

既然openfopen还要慢,还要麻烦,为什么我们还要用它呢? 因为,*nix系统中一切皆文件,不光有文本文件,还有很多设备文件、pipes文件。而不是所有文件都需要缓存。类似设备这种,需要都是立马就操作,而不希望等待操作缓存的时间。

所以除了文本文件外,广泛的文件处理还是要用open的,因为open是底层的,意味着你能有更多的细节控制。

简而言之,fopenopen相当于高级语言和低级语言。高级语言用起来方便,低级语言可控性强。

solomonxie commented 5 years ago

❖ Mac上的gdb之:从入门到放弃

副标题:Mac上的gdb无法正常调试的问题

Mac上用brew install gdb安装gdb后,无法正常的运行run命令,报错如下:

(gdb) break main
Breakpoint 1 at 0x100000f66: file a.c, line 4.
(gdb) run
Starting program: /Users/solomonxie/Workspace/tests/clang/a
Unable to find Mach task port for process-id 63414: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))

这个不是c程序的问题,也不是gdb的问题,而是Mac的问题。

参考:gdb doesn't work on macos High Sierra 10.13.3

为什么Mac不能调试?

"因为 Darwin 内核在你没有特殊权限的情况下,不允许调试其它进程。调试某个进程,意味着你对这个进程有完全的控制权限,所以为了防止被恶意利用,它是默认禁止的。允许 gdb 控制其它进程最好的方法就是用系统信任的证书对它进行签名。"

参考:gdb fails with “Unable to find Mach task port for process-id” error 参考:How to install and codesign GDB on OS X El Capitan

具体步骤如下:

开启root权限

用Spotlight搜索Directory Utility程序,打开后,点击左下角解锁,然后打开菜单->Edit->Enable root user->创建密码。

修改/System/Library/LaunchDaemon/com.apple.atrun.plist文件

将第22行的-s改为-sp然后保存退出。

一般来讲管理员是没有权限修改的,所以需要重启进入“安全模式”用root权限解开系统文件的保护,再重启,修改文件,再重启进入安全模式,再开启系统文件保护,再重启回到正常系统。 步骤为: 重启,黑屏时按住Ctrl-r不松手一直到苹果标志出现。进入安全模式后,打开菜单Utilities-Terminal终端,输入csrutils disable解锁系统文件保护。然后重启,回到正常系统中,sudo vim /System/Library/LaunchDaemon/com.apple.atrun.plist将文件中22行-s改为-sp,保存退出。重启再次进入安全模式,命令行输入csrutils enable锁定系统文件保护。再重启,回到正常系统,进行下一步。

删除所有现有的gdb版本:

brew uninstall --force gdb

打开系统的Applications -> Utilities -> Keychain Access删除所有gdb相关的证书。

重新安装gdb:

brew install gdb

创建证书

打开系统keychain管理器:Keychain Access, go to menu Keychain Access-> Certificate Assistant -> Create a Certificate

image

创建新的证书,所填内容如下:

Name : gdb-cert
Identity Type: Self Signed Root
Certificate Type : Code Signing
[X] Let me override defaults

Serial Number : 1
Validity Period (days): 3650

Key Size : 2048
Algorithm : RSA

[X] Include Key Usage Extension
[X] This extension is critical
Capabilities:
[X] Signature

[X] Include Extended Key Usage Extension
[X] This extension is critical
Capabilities:
[X] Code Signing

[X] Include Subject Alternate Name Extension

Keychain: System

为证书添加信任

在Keychain管理器里,双击刚刚创建好的证书,在Trust中全部选择为Always Trust:

image

重启taskgated并codesign将程序与证书关联

再打开命令行输入:

sudo killall taskgated
codesign -fs  "gdb-cert"  `which gdb`
launchctl load /System/Library/LaunchDaemons/com.apple.taskgated.plist

设置set startup-with-shell off

进入gdb调试程序,然后输入命令:

(gdb) set startup-with-shell off

然后正式开始调试。

如果调试没有问题,则将set startup-with-shell off这句话写入~/.gdbinit文件中,长久生效。

如果经历了这一切都没用,那么试试自己编译第三方gdb

因为看到有人是由于更新了gdb或更新了os系统后才遇到问题,所以想是不是gdb版本与当前os版本不合的问题。 所以决定自己编译别的版本gdb。

官方各个版本的下载地址:https://ftp.gnu.org/gnu/gdb/ (经过测试,我的在MacOS 10.12 Sierra上编译各个新老版本gdb都编译不成功)

开始下载编译:

cd /tmp
wget https://ftp.gnu.org/gnu/gdb/gdb-7.12.1.tar.gz
cd gdb-*/
./configure --prefix=/opt/gdb-7.12 && echo [  OK  ]
make && echo [  OK  ]
sudo make install && echo [  OK  ]

如果还是没用,那么需要针对自己的OS版本做调查了

我当前的系统是MacOS 10.12 Sierra。相关的说法是:

"None GDB 7.11 or 7.12.1 will not work on Sierra 10.12.4 In short it's because of Apple security upgrade. We need to wait for re-enabling when some new version will shows up."

顺着这条思路搜索,找到一个有人已经编译好的gdb二进制单文件。然后再用codesign给它签名,竟然就可以用了!

这里下载gdb7.12.1 sierra .zip 或在百度网盘下载

解压后,备份并替换本机的gdb,放到/usr/local/bin/中。然后pkill taskgatedcodesign -s gdb-cert /usr/local/bin/gdb进行签名。 但是直接gdb还不行,需要用sudo gdb ..才能正常用。

注意:重新安装gdb后。第三方软件如cgdb,需要重新安装才能使用,否则完全无法用。

最后的最后

Mac上LLDB才是王道。Xcode默认调试器是LLDB,说明了苹果不鸟GNU。也有人说,GDB是过去,LLDB是将来。虽然不一定正确,但也证明了LLDB也很强大。

再有一点最重要的理由:你的项目生产环境真的是在Mac上吗? 既然生产环境不在Mac,为什么要用Mac编译? 这个逻辑一想通,就全通了—— 一般生产环境是在Linux服务器上的,所以你大可以共享项目文件夹给服务器,然后SSH进服务器进行编译调试。 如果只是学习语言用的小文件,那么更没必要用到强大的GDB功能,在Mac本地用LLDB即可。

所以,唯一的缺点就是用不了各类GDB的衍生品、GUI一类,排除这点,还是安心用LLDB吧,不要在Mac上折腾GDB了。。。

solomonxie commented 5 years ago

❖ C Operators 操作符 [DRAFT]

参考Youtube:A Tour of C's Many Operators

Arithmetic Operators 基本运算操作符

Increment/Decrement Operators 增量操作符

Comparison Operators 比较操作符

Logical Operators 逻辑运算符

Member Operators 成员操作符

参考:What does "->" mean in C/C++ programming?

(重要)Bitwise Operators 二进制运算符

二进制运算符,就是先把数字转为二进制,然后对每一位进行运算。

XOR Gate: 电路逻辑中的绝妙设计,即如果有两条线路同时运行:即1^1或同时关闭:即0^0,那么线路就全部关闭。这样轻松的就解决了线路冲突问题。

(重要)Shift Operators 位移运算符

首先要确认位移的几个点:

左右位移:

左位移9<<2的详细步骤:

9 十进制数
01001 转为二进制
~~~~~ 先确认总位数为5位
010_01000 因为向左移动了3位,后面空出来的用0补足
01000 因为下划线左边的部分超过了长度,所以要被删除
72 转换回十进制

右位移9>>2的详细步骤:

9 十进制数
01001 转为二进制
~~~~~ 先确认总位数为5位
00001_001 因为向右移动了3位,前面空出来的用0补足
00001 因为下划线右边的部分超过了长度,所以要被删除
1 转换回十进制

快捷算法(数学意义):

(重要)运算符的顺序(优先级)问题

image

( (x>y) && ((a>b) || (a>c)) )
solomonxie commented 5 years ago

❖ All about Binaries [DRAFT]

二进制基础运算

二进制加减法

加法: image

减法: image

二进制乘除法

乘法: image

除法: image

二进制转换

参考:二、八、十、十六进制转换(图解篇)

二进制 -> 十进制

总之就是一个Σ Value * 2^index的过程。

假设有一个二进制ABCDE, 那么转换过程就是:

A * 2^4 +
B * 2^3 +
C * 2^2 +
D * 2^1 +
E * 2^0

比如二进制101011,那么最方便的就是我们竖着写(建议从下往上写):

1 * 2^5 +
0 * 2^4 +
1 * 2^3 +
0 * 2^2 +
1 * 2^1 +
1 * 2^0

以上简化一下就是:

2^5 +
0 +
2^3 +
0 +
2 +
1
= 43

十进制 -> 二进制

总之就是:不断除以2来取余数的过程,所有的余数按倒序排列出来就是二进制的结果。

image

假设十进制数43,转换为二进制的过程就是:

43 / 2 = 21   ---mod---> 1
21 / 2 = 10   ---mod---> 1
10 / 2 = 5   ---mod---> 0
5 / 2 = 2   ---mod---> 1
2 / 2 = 1   ---mod---> 0
1 / 2 = 0   ---mod---> 1

那么结果按照倒序取余数,结果是:101011

solomonxie commented 5 years ago

❖ C标准库详解 (#include <*.h>) [DRAFT]

C除了动态内存外,就语言本身而言,基本语法是非常好理解的,像Javascript一样简单。 只是,想用好C,就必须像Python一样知道各种内置函数、内置库。这样我们就不用重复造轮子了。 在C里,这个叫C Standard Library,即C标准库。

那么怎么引用标准库呢?——通过导入xx.h头文件。在讲标准库之前,先来讲讲*.h头文件。

<*.h> 头文件

本质上来说,*.c*.h是一模一样的:你可以在xx.h中写具体的代码实现,然后编译成可执行文件;也可以在别的文件中导入xx.c

实际上,你用xx.hhhxx.abc,是没区别的,一样可以在include中引用。只是习惯做法而已。

基于历史原因,各种编译器都已经明确了.c.h的优化编译方法。如果混淆来写的话,对编译器是不友好的,对别人的阅读也是不友好的。

"xx.h"<xx.h>的区分

include 的用法有两种,如下所示:

#include "myHeader.h"
#include <stdHeader.h>

当我们在include导入外部文件时候,编译器需要寻址"xx.h"用来指定库的具体路径(包括系统路径下的标准库和其它路径的第三方库),而<xx.h>只单纯指定系统路径下的库,也就是标准库。 也就是说,"xx.h"这种写法,多了一个功能:可以指定具体路径。

参考:C语言#include的用法详解(文件包含命令)

为了更友好的展示代码,我们还是保留<xx.h>是标准库,"xx.h"是第三方库的好习惯。

变量、函数的 声明 vs. 定义

简单说: