小明的贤鱼

小明的贤鱼 查看完整档案

海外编辑Korea University  |  Computer Science 编辑  |  填写所在公司/组织 github.com/xxMeow 编辑
编辑

while(true) -1s;

个人动态

小明的贤鱼 赞了文章 · 9月9日

Nginx配置跨域请求 Access-Control-Allow-Origin *

当出现403跨域错误的时候 No 'Access-Control-Allow-Origin' header is present on the requested resource,需要给Nginx服务器配置响应的header参数:

一、 解决方案

只需要在Nginx的配置文件中配置以下参数:

location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
} 
上面配置代码即可解决问题了,不想深入研究的,看到这里就可以啦=-=

二、 解释

1. Access-Control-Allow-Origin

服务器默认是不被允许跨域的。给Nginx服务器配置`Access-Control-Allow-Origin *`后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。

2. Access-Control-Allow-Headers 是为了防止出现以下错误:

Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

这个错误表示当前请求Content-Type的值不被支持。其实是我们发起了"application/json"的类型请求导致的。这里涉及到一个概念:预检请求(preflight request),请看下面"预检请求"的介绍。

3. Access-Control-Allow-Methods 是为了防止出现以下错误:

Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

4.给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误

发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法。

三、 预检请求(preflight request)

其实上面的配置涉及到了一个W3C标准:CROS,全称是跨域资源共享 (Cross-origin resource sharing),它的提出就是为了解决跨域请求的。

跨域资源共享(CORS)标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

其实Content-Type字段的类型为application/json的请求就是上面所说的搭配某些 MIME 类型的 POST 请求,CORS规定,Content-Type不属于以下MIME类型的,都属于预检请求:

application/x-www-form-urlencoded
multipart/form-data
text/plain

所以 application/json的请求 会在正式通信之前,增加一次"预检"请求,这次"预检"请求会带上头部信息 Access-Control-Request-Headers: Content-Type

OPTIONS /api/test HTTP/1.1
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
... 省略了一些

服务器回应时,返回的头部信息如果不包含Access-Control-Allow-Headers: Content-Type则表示不接受非默认的的Content-Type。即出现以下错误:

Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

参考文章:

阮一峰【跨域资源共享 CORS 详解】
MDN web docs【HTTP访问控制(CORS)】

查看原文

赞 93 收藏 76 评论 19

小明的贤鱼 发布了文章 · 7月24日

Python的包管理与虚拟环境

Tools

包管理:pip / conda

虚拟环境:virtualenv

pip

禁用全局安装

今天无意中把要装在虚拟环境里的包装成了全局,而且还是用的批量安装列表,按完回车发现忘记切换环境心都凉了。。幸好仔细一看发现大部分都是提示了已安装,但还是有两个漏网之鱼QAQ

为了避免惨剧再次发生,赶紧禁用了全局环境下的pip

~/.bashrc中加入如下命令(用zsh的往~/.zshrc写):

export PIP_REQUIRE_VIRTUALENV=true # Forbid pip install in global environment
gpip() { # global pip
    PIP_REQUIRE_VIRTUALENV="" pip "$@"
}

可以看到还顺便添加了一个函数,这样在确实需要全局pip时就可用gpip来代替了。

此时所有的pip命令都必须在虚拟环境中运行,即便是pip list这种单纯的查询函数也必须通过gpip来调用。。感到心安。

conda

virtualenv

创建虚拟环境
$ virtualenv testenv

当前目录下将出现一个名为testenv的文件夹

进入虚拟环境
$ source testenv/bin/activate

命令行提示符将带上(testenv)前缀

退出虚拟环境
$ deactive

前缀将消失,回到全局环境

删除虚拟环境

退出虚拟环境后删除整个文件夹即可

查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月15日

Unity下载安装Standard Assets及报错解决

Unity升级之后不仅不自带标准包,从asset store里下载这个包之后还报一堆错。。无语凝噎。在此记录一下。

下载安装Standard Assets

  • 打开Assets Store搜Standard Assets出来的第一个就是,选择add to my assets完成购买
  • 在Unity的导航栏打开Windows > Package Manager页面,左上角加号旁边有一个All Packages的标签(所以all packages并不显示全部的包!迷惑。。),点一下切换到My Assets,就能看到标准包了。
  • 点击标准包,选择好要导入的模块后点import,这些部分就会被导入到Assets\Standard Assets\目录了!

以下为导入后遇到的一些问题,按模块分类。。

Standard Assets\Utility

'GUIText' is obsolete
Assets\Standard Assets\Utility\SimpleActivatorMenu.cs(12,16): error CS0619: 'GUIText' is obsolete: 'GUIText has been removed. Use UI.Text instead.'

这个问题最坑的是提示的修改建议不完整!!
貌似Unity从某个版本开始不用GUIText而改用UnityEngine.UI里的Text了,双击报错信息会直接打开这个SimpleActivatorMenu.cs文件。首先在文件头部加一条

using UnityEngine.UI;

然后把报错位置的

public GUIText camSwitchButton;

改成

public Text camSwitchButton;

保存,回去就好了。

参考:https://answers.unity.com/que...

Standard Assets\Editor

Editor\Water\Water4下一系列type or namespace name ... could not be found的错误

更坑,连个提示也没有。。

这个Water貌似依赖Environment下的Water,重新打开导入标准包的页面,import一下Standard Assets\Environment\Water,解决。

参考:https://answers.unity.com/que...

查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月8日

[读] C和指针 (Ch15 ~ Ch18)

Chapter 15

  • (本章介绍I/O,涉及内容多为各种函数的细节,因此不一一整理)
  • 各不相同但容易混淆的三个概念:

    • 文件:物理存储介质上所保存的内容
    • 流:函数库为I/O所提供的接口
    • 缓冲:程序对流进行读写时所使用的一块空间(可以手动指定不要缓冲)
  • 文件和流之间的转换由操作系统完成,而程序只是通过FILE数据结构来管理储存流和缓冲区的I/O状态

Chapter 16

  • 断言<assert.h>

    • 通过断言某个值为真来进行测试

      void assert(int expression); // 断言expression的值为真
      • 断言指针非空:

        assert(ptr != NULL);

        若条件不为真,则终止程序并打印表达式及断言位置:

        > Assertion failed: value != NULL, file.c line 274
    • 可以通过#define NDEBUG在编译时忽略所有断言而不用修改源码,当然命令行参数-DNDEBUG也有同样效果

Chapter 17

  • (本章介绍各种数据结构,不一一整理)

Chapter 18

  • (本章介绍C编译为汇编的细节,不过已经用ARM学过啦,所以没有细读,以下为简单的笔记)
  • 静态变量和它的初始化:编译器在程序的数据区(.data)为静态变量创建空间并初始化
  • 堆栈帧(stack frame)

    • 堆栈中的一个区域
    • 每一次函数调用都产生一个堆栈帧,用于保存局部变量和其他值
    • 帧指针指向堆栈帧的起始位置(因此每次函数调用中的帧指针都不同,使用link / unlink来切换帧指针)
    • 堆栈帧中的所有值都通过使用帧指针(frame pointer(不是 stack pointer))加上一个offset来访问
    • 参数:逆序入栈(这样可以通过对帧指针加上一个递增的常数offset来顺序访问参数)
  • 函数(.text区)

    • 函数序(prologue):准备工作

      • 为函数的局部变量保留堆栈中的内存(创建堆栈帧)
    • 函数体(body):

      • 对变量和函数的单纯声明不会产生指令,但初始化产生对应的赋值命令
    • 函数跋(epilogue):负责在返回前清理堆栈(事实上并不完全清理,因为只有调用者才知道参数个数)
  • 同一个编译器产生的程序才能被互相链接
查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月8日

[读] C和指针 (Ch11 ~ Ch14)

Chapter 11

  • mallocfree维护一个内存池

    • malloc总是分配一整块内存。根据编译器的实现,实际分配的内存也有可能比请求的稍大一些
    • 可用内存池无法满足请求时,malloc先向OS申请新的内存,还是不够时返回NULL。因此malloc返回的指针必须先检查
    • free的参数必须为通过malloc等函数申请到的内存指针或NULL,它也总是释放整个分配到的内存
  • calloc:返回指针前将整块内存都初始化为0
  • realloc

    • 用于(在尾部)扩大或缩小一块已经分配到的内存,可以被用于有效地释放一块内存的尾部空间
    • 原先的内存无法修改大小时realloc可能返回一块新内存的指针(原先的内容会被复制过去),因此realloc后必须更新指针
    • 第一个参数为NULL时,与malloc行为相同

Chapter 12

  • 可以额外使用一个根结点来避免插入发生在链表头部以至于需要修改根结点的值。对于双链表,额外的根结点还可以同时维护链表的头部和尾部
  • 不过当一个节点本身的体积过大(比如存有大块数据)时,这种方式会造成浪费

Chapter 13

  • ⚠️函数指针(这里太重要了吧!!)

    此处重点在于函数调用符(即函数名后的括号)的优先级比间接访问要高,因此

    int *foo(); // foo是一个返回int*指针的函数

    foo优先与函数调用符结合,成为一个函数,然后对该函数的值(即返回值)进行间接访问得到int,可据此推论函数返回值为int*

    int (*foo)(); // foo是一个返回int的函数的指针

    foo被迫先与间接访问符结合,也就是说*foo作为整体与函数调用符结合,即*foo为函数,则foo为函数指针

    int (*foo[])(); // foo是一个函数指针数组

    foo先与[]结合,说明foo是一个数组名,接着foo[]*结合,说明foo中的元素为某种指针,最后*foo[]与函数调用符()结合,并参照类型声明为int,说明元素为返回int值的函数指针(晕了。。)

  • 函数名在表达式中的角色与数组有些类似,也是以类似指针常量的形式出现的,因此

    void f(int arg); // f为接收1个int参数,无返回值的函数
    
    void (*fPtr)(int arg) = f; // 函数名可以作为指针被直接复制给函数指针
    void (*fPtr)(int arg) = &f; // 也可以先取址再赋值给函数指针

    事实上,&只是显式地执行了编译器将隐式执行的工作

  • 转移表(jump table)常通过函数指针数组实现,此时若发生数组越界等问题debug将会非常艰难。。
  • 命令行参数

    • argc:参数个数(即argv数组的长度)
    • argv:字符串指针数组(命令行中空格分隔的每一截都为一个参数),通常通过检查字符串开头是否为-来识别选项参数
  • 字符串常量本身其实就是一个常量指针

    char *strPtr = "abcdefg";
    
    "abcdefg"[2] == 'c'; // true
    "abcdefg" + 2 == strPtr + 2; // true,因此它也可以作为一个字符串指针在printf里直接用%s输出

Chapter 14

  • 预定义符号(不是预处理指令!)

    • __FILE__%d源代码名(test.c
    • __LINE__%d文件当前行号(25
    • __DATE__%s文件被编译的日期(Jan 31 1997
    • __TIME__%s文件被编译的时间(13:07:02
    • __STDC__%d编译器是否遵循ANSI C标准(1 / 0
  • ⚠️带参数宏

    #define macro(arg1, arg2) stuff // 参数列表的左括号必须紧邻宏名,否则就会被认为是stuff的一部分

    (以前对宏有误解,以为是单纯的把一段文本替换成另一段文本,而实际上宏是先被按照符号解析再转换为文本进行替换的)

  • 宏很容易产生歧义,要格外注意括号和分号的使用
  • 宏参数和#define定义可以包含其他#define定义的符号,但宏不可以递归(大概是因为没有调用栈这种东西)
  • 利用printf会将直接相邻的字符串连接起来的特性,可以实现简单的输出包装:

    #define PRINT(FORMAT, VALUE) printf("This value is "FORMAT".\n", VALUE) // 注意没有分号!
    
    PRINT("%d", x + 4); // 则FORMAT为"%d",VALUE为x + 4
    // 实际被转换为:
    printf("This value is ""%d"".\n", x + 4); // 三个字符串被自动连接
  • ⚠️符号连接符##:产生的新符号必须合法,否则就是undefined行为

    #define ADD_TO_SUM(sum_number, value) sum##sum_number += value // 注意没有分号!
    
    ADD_TO_SUM(5, 23); // 5将作为sum_number的值被连接到sum,产生sum5这个符号,最后整个宏被替换为sum5 += 23;
    // 宏被引用时上下文中必须存在名为sum5的变量
  • ⚠️宏接收参数不指定类型,因此甚至能接受类型本身为参数

    (这就是为什么sizeof不是一个函数吧,因为类型本身不能作为参数传递,以及可变参数列表也是用宏来实现)

  • 带副作用的宏参数可能导致严重错误,比如:

    • ++ / --:改变参数本身
    • getchar():消耗I/O buffer
  • 因为宏是被直接替换的,所以反复使用同一个宏也总是生成新的代码。而函数则每次在调用时使用同一份代码
  • 命令行定义:

    • -D{name} / -D{name}=stuff === #define name stuff (未指定时stuff默认为1)
    • -U{name} === #undef name
  • 条件编译:#if / #endif / #elif / #else

    • 条件表达式将由预处理器进行求值,因此它们的值必须是在编译时就能确定的常量
    • 测试符号是否已被定义:

      • #ifdef symbol === #if defined(symbol)
      • #ifndef symbol === #if !defined(symbol)
    • ⚠️可以结合命令行定义来完成条件编译

      • 源代码test.c

        #ifndef CHOICE // 以免与命令行定义冲突
            #define CHOICE 5
        #endif
        
        int main() {
            printf("Choice: %d\n", CHOICE);
            return 0;
        }

        直接编译执行:

        > gcc test.c -o test
        > ./test
        > Choice: 5

        编译时使用命令行定义:

        > gcc -DCHOICE=2 test.c -o test # 提前定义CHOICE为5
        > ./test
        > Choice: 2
    • 可以在#endif后用注释标注它结束的是哪一个条件以提高程序可读性
    • ⚠️使用条件编译来避免由于头文件被重复包含导致的符号冲突

      #ifndef _HEADERNAME_H
      #define _HEADERNAME_H 1 // 如果没有1则_HEADERNAME_H被定义为空字符串,但仍然被定义,不会影响判断结果
      #endif
      • 尽管有办法防止冲突,多重包含仍然会拖慢编译速度,应被尽力避免
      • 但也不应该为了避免多重包含而依靠头文件自己嵌套包含,这样会导在使用make等工具时很难判断文件间的依赖关系
    • #progma指令是不可移植的,不同平台的预处理器可能会完全忽略它,或以不同的方式执行它
查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月7日

[读] C和指针 (Ch8 ~ Ch10)

Chapter 8

  • ⚠️数组名和指针常量相似,但并不是指针!(也因为是指针常量,所以数组名也是un-assignable的)

    • 编译器用数组名来记住数组的属性,只有当数组名在表达式中被使用时,编译器才为它产生一个指针常量
    • 只有两种情况下数组名不用指针常量来表示:

      • 用于sizeof:对数组名使用sizeof将返回整个数组的长度(以字节为单位)
      • 用于&取址:得到指向数组的指针(而不是指向第一个元素的指针的指针)
    • 下标引用可以作用于任何指针,而不仅仅是数组名,因此C编译器不检查下标的范围,下标也可以为负
    • 通过指针来访问数组中的元素可能比用下标的效率更高,因为通过下标访问数组总是要做以下运算:

      arr + (index * sizeof(elementOfArr)) // 此处的乘法无法避免

      而某些情况(主要是循环体)下,对该偏移值的计算将被编译器优化为纯加法(一个在现代基本没什么用的知识。。):

      for (int* arrPtr = arr; arrPtr < arr + 10; arrPtr ++) { ... } // arrPtr++ 所加上的值只需要计算第一次
    • 一个注意:

      指针不仅在做加法时需要考虑元素大小,两指针相减的结果也会被通过除法转换为元素个数(单纯比较是否相等则不需要)

  • ⚠️函数接收多维数组做参数时,必须指定除第一维外其他所有维度的大小,否则寻址时函数无法计算偏移量

    void func(int x, int y, int arr[][]) { // 未指定第二维大小
        printf("%d", arr[x][y]); // 访问arr的第(x * sizeOfRow + y)个元素,但函数不知道sizeOfRow
    }
  • 字符数组的初始化与字符串常量

    char msg[] = "Hello";
    char *msg = "Hello"; // 字符串常量是read only的
  • ⚠️声明一个指向数组的指针:

    int arr[3][10];
    int (*arrPtr)[10] = arr; // arrPtr是一个指向拥有10个int元素的数组的指针,arrPtr + 1将指向下一行的10个int的开始

    多维数组可以通过以下形式传递给函数:

    void func(int (*mat)[10]);
    void func(int mat[][10]);

    当多维数组各维度的大小并不总固定时(我的最爱~),可以通过以下方式将它以一维数组的形式传递给函数:

    int *arrPtr = &arr[0][0];
    int *arrPtr = arr[0];

    但是以下这种声明可能会导致严重的问题:

    int arr[3][10];
    int (*arrPtr)[] = arr; // 此时指针arrPtr不知道数组arr的size信息,它将会把0当作arr的二维宽度(而不是3)
  • 下标引用[]的优先级高于间接访问*

    int *p[10]; // p被声明为拥有10个int*元素的数组

Chapter 9

  • strlen的返回值为size_t,是一个unsigned值(永远大于等于0)
  • 标准库中的字符串函数不负责安全性检查,程序员必须保证字符串以NULL结尾(在必要的情况下)且大小合适
  • strcpystrcat等函数会将目标字符串的地址作为返回值,这个特性令它们可以被嵌套使用
  • 即使是对转换大小写这种简单的操作,使用库函数也有利于提高程序在使用不同字符集的平台间的移植性
  • strerror函数接收一个错误代码,并返回一个指向描述该错误的字符串的指针

Chapter 10

  • ⚠️struct(突然发现我根本不知道struct的标准语法。。)

    struct Tag { // struct关键字和花括号之间的部分为该struct的标签,为可选部分,这样声明的结构体每次使用时都要加上struct关键字
        int val;
        char name[10];
    } tag1, *tag2; // 此处可以顺便声明该类型的变量,但此后声明新变量必须使用struct Tag tag;,因为Tag实际上不是一种类型
    
    typedef struct { // 将这种struct定义为一种类型,因此以后使用时不用加struct关键字
        int val;
        char name[10];
    } Tag; // 以后可以直接使用Tag tag;来声明新变量

    所以struct {};才是一个整体,它可以被加上标签作为记号,也可以被typedef为一种新的结构体

    (我竟然一直以为是在主函数外声明新变量就要在类型前加struct关键字。。所以我这么久都写的啥==)

  • 点操作符.的优先级高于间接访问*,所以C提供了->来更为方便地通过结构体指针访问它的成员(原来如此!!)

    (*ptr).val = 0;
    ptr->val = 0; // 显然下面这种写法更为直观

    以及优先级上->高于.高于*

    *p->a.b; // 将会从p指向的结构体中取出a结构体的b成员并对它进行间接访问,即*((p->a).b)
  • 不完整声明:使两个(或更多)结构体可以互相包含对方的指针

    struct B; // 提前声明B的存在
    
    struct A {
        struct B *bPtr;
        ...
    };
    struct B {
        struct A *aPtr;
        ...
    };
  • 从C2011开始typedef struct foo foo;是合法的了,但是这种写法在之前的编译器上(可能!)会出现兼容性问题
  • ⚠️C中的空结构体为undefined行为(但C++允许结构体为空),以及结构体实际上允许嵌套声明,因此可能无意间声明空结构体:

    struct A {
        struct B { // 此处实际只是struct B的定义,而非变量声明。因此没有其他成员的struct A实际为空结构体
            int id;
            ...
        };
    };
  • ⚠️对结构体使用sizeof将返回它实际占用的字节数,包括那些为了边界对其而跳过的字节。

    因此要确定一个成员对于结构体起始位置的实际偏移量,应使用offset(type, member)宏(位于stddef.h),type为结构类型,member为该成员的名字。这个宏将返回一个size_t

  • 把整个结构体作为函数的参数or返回值都会导致整个结构体被复制,使用指针效率更高
  • 位段(bit filed):(本质上是不可移植的。。hmmm)

    • 位段成员必须为intunsigned int类型
    • 成员后跟一个冒号和一个用于指定该位段所占用的bit数的整数
    struct CHAR {
        unsigned ch :   7;
        unsigned font : 16;
        unsigned size : 19;
    };
    struct CHAR c1;
  • ⚠️联合(union):(通过访问不同的联合成员,内存中的同一块内容可以被解释为不同的东西)

    • struct相似,union也允许嵌套声明

      struct POINTER { // 实际包含两个变量,type和ptr
          enum {INT, FLOAT, STRING} type;
          union {
              int        *i;
              float    *f;
              char    *s;
          } ptr; // 匿名union
      };
    • 联合中所有成员都拥有相同的起始地址,整个联合的大小取决于最大的成员
    • 为了避免个别超长成员造成空间浪费,通常只储存指向不同成员的指针,使用中再为该成员动态分配内存
    • 初始化:初始值必须为第一个成员的类型,且必须用花括号包围

      union {
          int a;
          float b;
      } x = { 5 }; // 其他类型的初始值(如果可能的话)将会被转换为一个int赋值给x.a
查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月5日

[读] C和指针 (Ch4 ~ Ch7)

Chapter 4

  • if嵌套出现时,else属于离它最近的不完整的if
  • switch的最后一个case也加上break是个好习惯,以免在后面追加新的case时忘记添加break
  • 使用goto一般不是好事,唯一的例外可能是需要从多重循环中直接退出时比较方便,但是这也可以通过把整个循环包装成一个函数,然后在需要退出的时候直接return来解决
  • C本身不具备任何I/O函数及异常处理能力,这些都要靠调用库函数来实现

Chapter 5

  • 取模运算符%不能用于浮点数
  • 对负数做整数除法的结果取决于编译器实现,可能向0取整,也可能向负无穷取整
  • 移位⚠️C只规定对于unsigned值使用逻辑移位,对于signed值使用的移位方式则由编译器决定!

    • 逻辑移位:不要问就是移!不管左右,空位全补0
    • 算数移位:

      • 左移时与逻辑移位效果相同,结果可能与原值不同正负,但这被视为溢出,不予处理
      • 右移时只能一位一位移,符号位为0则补0,为1则补1,以保证与原值同号

        • 正数的算数右移等价于逻辑右移,但负数不等价!
        • 负数不等价
    • 循环移位:汇编及verilog中用的桶移位(bucket shift)是循环移位,只有它确保原值中的0和1个数不变
  • ⚠️C中的赋值符实际上会将左值作为运算结果返回,所以下面这种连续赋值其实是合法的:

    int x = 3;
    int y = x = x + 2; // 合法

    但是实际上无论x是否为intx + 2都会返回一个int结果并赋值给x,而若x其实是一个char,这样的赋值就会导致结果的高位被截去,因而得到的x的值的完整性是无法保障的,进而y的值也不一定符合期望。

  • ⚠️sizeof操作符判断它的操作数的类型长度(sizeof不是函数!!),而并不需要知道它的操作数(也可以是表达式)的实际值

    // 括号
    sizeof(x);
    sizeof x; // 两者完全等价,以字节为单位返回变量x的长度
    // 数组
    sizeof(arr); // 返回数组的字节数(不是数组长度!!)
    // 表达式
    sizeof(a = b + 3); // 判断长度不需要知道表达式的实际值,因此sizeof不会执行接收到的表达式,a也并不会被赋任何值
    // 给sizeof一个有副作用的表达式将会得到如下警告:
    // Expression with side effects has no effect in an unevaluated context
  • ⚠️自增 & 自减

    ++和--真是C永恒的话题。。
    • 只能用于左值
    • 结果为值的拷贝,并不是变量本身。因此表达式中出现自增自减时,实际参与表达式运算的是一个un-assignable的单纯的值
    int a = 10;
    int b = a++;        // 合法:a加一的结果先被作为一个值拷贝后赋值给b,然后a被加一
    int c = (++a)++;    // 非法:++a实际是a值的拷贝,不是一个左值,因此不可自增自减
  • 逗号操作符分隔多个表达式,表达式们从左到右逐个求值,一整条逗号表达式的值等于最后一个小表达式的值。

    唯一的用处可能是用来简化同时出现在while循环前面和内部的语句(可以用逗号连接起来放进while的条件里,然后把原来的条件作为最后一个表达式)。。因为while每一轮开始前都会执行一遍条件
  • 对于一个任意的整形值,显式地测试它的具体值比把它当作布尔值直接判断真假要更好
  • ⚠️隐式类型转换

    C中的整型算数运算至少都会以缺省整型类型的精度来进行,即便所有操作数都是更小的类型(竟然是这样!):

    char a, b, c;
    a = b + c; // 虽然a, b, c全都是较小的char,b和c也会先被提升为int,然后执行加法得到int结果,再将结果截短赋值给a

    但是,如果直接参与运算的值已经达到了不用提升的标准而运算结果将会产生溢出,那么即使左边变量足够接收完整的结果,得到的也是先溢出再被加长的值:

    int a, b;
    long c = a * b; // 就算a和b相乘产生溢出,右边表达式的计算结果也只是溢出后的int,不会因为c够大就提升a和b的长度
  • 对于左右都有表达式的运算符,先计算左边表达式还是先计算右边表达式是由编译器决定的,因此如下表达式的结果实际无法预测:

    a = ++b + --b; // C只规定自增自减运算要优先加法运算执行,但是先求加法的左边(++b)还是右边(--b)则由编译器决定

    有更好的例子如下:

    f() + g() - h();
    // 同级运算符从左到右执行的规则只约束例子中的加法比减法先执行,而不约束执行加法时一定要先算f()再算g()

    甚至编译器也可以先把每一个小的表达式求出来再做运算:

    f() + f() * f();
    // 先无视运算优先级,直接求出3个f()再进行整体运算也是可以的,此时3次f()的调用顺序完全取决于编译器实现

Chapter 6

  • 访问一个指针所指向的地址的过程称为间接访问(indirection)或解引用指针(dereferencing the pointer)
  • 注意运算符优先级,尤其是对被解引用的同时自增自减的指针(到底为什么要写这种代码!!)
  • 指针与整值的加减法总是以它所指向的类型的大小为单位:对int型指针+1会使它指向下一个int
  • ⚠️指向同数组两个指针相减的结果也是以它所指向的类型大小为单位的,这个结果的类型为ptrdiff_t
  • 指向不同数组的指针相减是undefined的,因为没有意义
  • ⚠️标准允许将指向数组元素的指针和指向该数组最后一个元素后面那个位置的指针相比较,但不允许将它和指向数组第一个元素之前的内存位置的指针相比较

    这是什么神仙规定。。不过大部分情况下查得不严,还是可以比较的,只是移植性会稍微下降

Chapter 7

  • 可变参数列表

    • stdarg.h

      • 类型:va_list
      • 宏:va_startva_argva_end
      • #include <stdarg.h>
        int sum(int argNum, ...) { // 对未知个数的参数求和
            va_list args;
            va_start(args, argNum); // 使用va_start(可变参数列表 + 最后一个命名参数)将args指向可变参数列表的第一个元素
        
            int result = 0;
            for (int i = 0; i < argNum; i ++) {
                result += va_arg[args, int]; // 使用va_arg获得可变参数列表中的下一个参数并为它指定类型
            }
            va_end(args); // 使用va_end结束访问可变参数列表
        
            return 0;
        }
      • 这些宏无法知道实际参数的数量和类型,并且可变参数实际上会先进行缺省参数类型提升再传入函数,要格外小心
查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 发布了文章 · 7月4日

[读] C和指针 (Ch1 ~ Ch3)

Chapter 1

  • 使用预处理语句“注释掉”代码最安全

    #if 0
        // some statements
    #endif
  • printf返回输出的长度(包括不可见字符),scanf只返回1(成功)或0(失败)
  • scanf读取输入到数组时,直接写数组名和&数组名效果相同

    int array[10]; // 以下两种方式效果一致
    scanf("%s", array);
    scanf("%s", &array);
  • 赋值运算符的返回值大概率为左值(C)或左值的引用(C++),但归根到底是一个undefined行为,取决于编译器的实现。但以下用法通常可以接受:

    while ((ch = getchar()) != EOF && ch != '\n') // clear input buffer
        ; // 分号单独写一行,以免把下一行代码当成循环体

    且此处ch通常被声明为int,因为EOF实际上是一个int

  • 将作为参数接收的数组名声明为const,可以防止意外修改数组中的数据
  • gets函数的安全性问题在于,它读取字符串时无法知道其长度,当遇到超长字符串时很容易溢出

Chapter 2

  • 程序的编译与执行

    • 首先看几个容易混淆的扩展名

      • .out / .exe 可执行文件,Linux和Unix下用.out,Windows下用.exe
      • .o / .obj 编译后产生的尚未链接的中间文件,机器码的初步形式,Linux和Unix下用.o,Windows下用.obj
      • .bin 单纯二进制文件,可能是代码,也可能是数据
    • 编译

      • Compile: source code > object code(对每一个.c生成对应的.o / .obj
      • Link: object code > executable(链接所有.o / .obj文件生成一个完整的.out / .exe
    • 执行

      • 程序被操作系统加载入内存
      • 执行:使用运行时堆栈及静态内存
      • 终止

Chapter 3

  • 字面值

    • 数字

      • 在不明确指定类型时,它是能完整容纳该值的最短类型
    • 字符

      • 使用L前缀指定字符为宽字符常量(unicode只是宽字符编码的一种,两者并不相等!

        wchar_t = L'X';
    • 字符串

      • ⚠️指向字符串常量的指针可以被指向别的地方,但字符串常量是不能被改变的!

        char* strPtr = "blahblah";
        strPtr[2] = 'D'; // 非法操作!!
        strPtr = "Hmmm"; // It's Okay..
  • 没有unsigned的浮点数!(第一次知道。。一个没有想过的问题)
  • 声明指针时,把星号紧挨变量写是一个好习惯:

    int* a, b, c; // 很容易误解为a、b、c三个指针,而实际只有a是指针,b和c仅仅是int
  • 在声明字符串指针的同时赋值实际意味将该指针指向字符串的首地址,毕竟C中根本没有真正的字符串类型

    char *message = "Hello";
    // 实际上等价于
    char *message;
    message = "Hello"; // 而不是 *message = "Hello";
  • 用typedef定义指针类型

    之前一直不能理解这种习惯,觉得真的很容易把指针误认为数据,命名时加个Ptr后缀不好吗??现在才知道原来是方便定义函数指针时用的。。soga

    #define charPtr char* // 危险!
    charPtr a, b, c; // 只有a被声明为指针,b和c为char数据
    // ------------------------------------------------------
    typedef char *charPtr; // It's Okay..
    charPtr a, b, c; // a、b、c的类型都为char*
  • ⚠️const(只表示道义上不会修改,硬要改还是可以用别的指针轻易绕开限制,防君子不防小人。。)

    • 声明常量

      const int a;
      int const a;
      • 两种方式完全等价
      • 函数中声明为const的参数被调用时会得到实参的值(?这句话没有理解)
    • 声明指针

      理解: 类型 、星号、变量名,这三个部分的相对顺序总是固定的,星号左边的const修饰数据,星号右边的const修饰指针

      • 指向常量的指针变量(完全等价)

        int const * p;
        const int * p;
      • 指向变量的常量指针

        int * const p;
      • 指向常量的常量指针

        int const * const ptr;
  • ⚠️链接属性

    • 种类

      • external - 不论声明多少次,位于几个源文件,都表示同一实体
      • internal - 统一源文件内的声明都为同一实体
      • none - 每次声明都是不同实体
    • 手动改变链接属性

      • extern 指定为external(代码块内部使用extern声明变量可以确保使用的是最外部的全局变量)
      • static 将缺省属性为external的声明转变为internal属性
  • ⚠️声明 vs. 定义(重新理解了这两个名词。。)

    • 声明Declaration

      • 仅仅是一个符号,不分配空间
      • 同一变量可声明多次
      • external声明不是定义
    • 定义Definition

      • 定义也是声明,真正地分配空间
      • 一个变量只能定义一次
      • 所有带初始化的语句一定是定义。
  • 储存类型

    • 静态变量 - static

      • 静态变量的初始化将会随着程序被加载入内存同步完成,完全没有额外开销
      • 未显示指定值的静态变量初始化为0
      • 用途

        • 用于函数声明或代码块之外的变量声明:将external的变量改为internal(反正作用域和生命周期已经最大了)
        • 用于代码块内部变量声明:将自动变量改为静态变量(作用域和链接属性不变)
    • 自动变量 - auto

      • 除了静态变量和极少量寄存器变量外所有的变量都为自动变量
      • 初始化和后期赋值的开销相同(除了const,毕竟不存在后期赋值)
    • 寄存器变量 - register
查看原文

赞 0 收藏 0 评论 0

小明的贤鱼 赞了文章 · 2019-09-29

那些我希望在一开始使用 Zsh(oh-my-zsh) 时就知道的

其实我已经用了 fish shell 快半年了,因为被一些兼容性问题搞烦了,所以最近又用回了 zsh + oh-my-zsh。

zsh + oh-my-zsh 配合是真心好用,而且 oh-my-zsh 还偷偷干了很多人不知道的事,比如 rake task也能补全,第一次见到时真是有点惊讶。好吧,下面说一说我希望一开始就知道的。

自带的插件

其实我用了 oh-my-zsh 快三个月后才知道原来他自带了很多插件没有开启。。。

如何开启?找到 ~/.zshrc 有一行 plugins=(git),想加什么插件就把名字放里面就是了,比如plugins=(rails git ruby) 就开启了railsgitruby 三个插件。

更多插件请进入 ~/.oh-my-zsh/plugins文件夹探索,也可以看看 wiki 里的 Plugins Overview,每个人的需求不一样,里面有一些比较神奇的插件,比如敲两下esc 它会给你自动加上 sudo 的 sudo 插件,让复制显示进度条的cp插件,解压用的 extract 插件(有没有觉得在命令行下敲一大堆选项才能解压有点奇怪?),vi 粉的vi-mode 等等...

z

嗯,这也是个自带的但是没有开启的插件。为什么单独把它拿出来讲呢?因为太好用了,没有它我根本就不想用命令行。简直就是可以无脑跳跃,比如你经常进入~/Documents目录,按下 z doc 一般它就可以跳进去了(当然首先你得用一段时间让它积累一下数据才能用)。类似的插件还有好几个比如autojump,fasd,这类东西好像叫 FS Jumping,这里有个完整列表:https://github.com/robbyrussell/oh-my-zsh/wiki/Plugins-Overview#fs-jumping

d

在 fish shell 里你可以按alt 加左右箭头随意穿梭于历史记录中,就像 GUI 文件浏览器的
前进,后退。zsh 没有这么好用,但是按一下 d 再回车你会看到最近的历史记录,然后你就可以通过数字比如 1, 2 之类的返回到某个历史记录中了。也是非常好用的。

zsh-autosuggestions

官方的介绍是Fish-like fast/unobtrusive autosuggestions for zsh,没错,这是模仿fish shell 的一个插件,作用基本上就是根据历史记录即时提示。没有这个东西让我感觉自己很盲目。没有用过 fish 的同学可能觉得它有点奇怪,但是一旦适应它以后就会发现它会大幅度的提高效率(按 ctrl+E 是正确姿势)。注意:它不是 oh-my-zsh 自带的,大家可以去 github 看它的安装说明。

自带的 Git Aliases

git 插件是默认开启的,大家可能注意到它能够自动补全以及显示所在 branch。对于一个 git 重度用户,一些 aliases 是不可缺少的。相信我,一旦习惯了 aliases,就会觉得原来敲那么一大堆命令真是不能忍了(即使它能够自动补全)。下面列一些我最常用的(当然也是 oh-my-zsh 自带的,更多请看 wiki):

AliasCommand
ggit
gstgit status
gagit add
gpgit push
gcgit commit -v
gc!git commit -v --amend

其他

  • zsh_stats 可以看到你的使用频率前 20 的命令是什么!

  • take 看看 which take 就知道它有什么用了

  • clipcopyclippaste,剪贴板和命令行的交互

官方的 wiki(重要!)

官方 wiki 上面给出了很多有用的功能介绍,比如一些 aliases,像..., ...., take 等等。

如果完整的看完了 wiki 的话我前面的介绍都是废话了。

结语

  • 推荐一个不相关的:trash-cli,就是命令行版的回收站,它的神奇之处在于不是简单的把文件移动到回收站,而且可以在回收站里恢复。所以它说自己是Command line interface to the freedesktop.org trashcan,我的 zshrc 里有一行:alias rt="trash"

  • 其他的一些非常有用的 tips 想到再补充。

  • 补上我的配置文件们:https://github.com/dd1994/dotfiles

查看原文

赞 30 收藏 81 评论 10

小明的贤鱼 赞了回答 · 2019-08-02

撤销git add

其实可以不用记住那么多,内次可以用 git status 他会告诉你可以通过那个命令来执行操作

关注 26 回答 16

认证与成就

  • 获得 0 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-16
个人主页被 428 人浏览