C「帶壞了」多少程序語言的設計?

希望回答者不要局限於 C 本身,盡量提及下受其影響的其他語言。


這是一個語法可以影響人類大腦思考方式的絕佳例子。Pascal和C到底有什麼區別?其實除了內置一個255長度的string和set和本地函數以外幾乎就沒有什麼區別了,只是換種寫法而已。那為啥C語言有void*濫用,pascal沒有pointer濫用?為啥C語言有NULL的問題,pascal沒有nil的問題?為啥C語言容易跑著跑著就飛掉,pascal寫起來就會放心一點(雖然問題也沒有完全解決)?

因為C幹壞事需要的字元太少了,pascal要是干點什麼壞事,一眼看上去就是在幹壞事,無形中就起到了小黃鴨code review你的精神效果。

這個時候就應該給一個絕佳的例子:

C:

char a = "f";
strcpy(a, "fuck!");

Pascal:

var a : char;
begin
a := "f";
{ 一個表達式好像做不了這件事情 } := "fuck!";
{ 升級到delphi之後可以這麼寫 }
StrPLCopy(@a, "fuck!", 5);
{ 操作字元串的時候,用奇怪的函數而不是:=,一股酸味(逃 }
end;


你是想看 C Family 譜系?

Stanford 這張圖不錯

https://ccrma.stanford.edu/courses/250a-fall-2005/docs/ComputerLanguagesChart.png


當初估計那幾個大爺也沒想到,有一天一群彙編都沒學清楚的人會用到C吧。


我偏個題。

有一次我在一個群里念叨++運算符之害,結果就有人冒出來質問我知不知道左右值的概念,我說不知道,他就哈哈大笑,說我裝逼被打臉。

當然了我是不知道左右值的概念,我只知道,需要你調出右值這種概念的地方,比如(a&>b ? a : b)=1,其實用取地址操作符就可以解決,比如*(a&>b ?a:b)=1,只要記住「取地址*a=1表示對a指向的地方賦值」即可;

我也不知道++運作的機制,我只知道,a[i++]=1這種裡面套個i++的表達式,只有在很少的情況下while循環的條件里可能用得上,而這時你寫(i=i+1,a[i-1]=1)即可。

這些概念,你在編程里用了,除了把自己繞暈以外什麼用也沒有,但是就是有人以熟練掌握它們為榮。我覺得,那個笑我的人,正是所謂的「下士聞道,大笑之,不笑不足以為道」。

從前lisp的動態變數,只不過是起初寫解釋器的人留下的bug,後來lisp大行,卻一直沒改,結果直到1992年的論文裡面,還有一堆人說「動態變數是lisp區別於其它語言的特徵」。於是我發現,一個語言只要流行起來,以掌握它所有細節為榮而不管這些細節是有用還是有害的人就特多,而這對後來的語言,肯定是不利的。


帶著偏見的問題,c以後的很多語言都試圖做更好的c,讓它更容易使用,並且享受它本來就有的好處,但是如同所有偉大發明的拙劣模仿者,它們註定不會成功,這不是c帶壞了誰,而是這些後續者貪功短見造成的。偉大的作品是不能寫續作的,即使是它本來的作者。偉大的作品也是不能模仿的,因為你永遠走不出它的影子。真正超越c的語言一定是徹底擺脫它的影子的獨創者、顛覆者,而不會是那些被它「帶壞了的」、看上去很像的語言,可惜,這樣的語言現在好像還沒有誕生(或者我不知道)。


C說.好好好,兒子孫子都長大了,嫌棄我了...


c最大的失誤是基礎類型的定義不是標準化的而是平台相關的,比如int可以是8位16位32位,這就導致了直接使用基礎類型定義的代碼無法跨平台編譯,於是搞出了個打補丁的方式用typedef來定義平台無關的類型,但這又導致了命名的隨意性讓類型失去了統一的標準,比如int32,Int32,dword之類。

另外c本身的類型命名也存在著隨意性,比如short,long,unsigned,double之類命名缺乏一致性,也給後期擴展類型造成了困擾,比如64位int要叫long long,太扯了,將來128位int怎麼辦?叫long long long嗎?現在回過頭看,比較好的命名方式應該像這樣:int8,int32,uint8,uint64,float32,float64,char8,char32,命名一致易於擴展又可以做到平台無關,方便移植。

因為c的廣泛使用讓基礎類型的命名成了約定俗成的事實標準,所以後面的大部分強類型語言都沿用了c的類型名字,當然也做出了改進,對類型進行了標準化,不再是平台相關的了。但是依然保留了short,long,double這種讓人迷惑的名字。


只稍微了解一些 C++ …

話說這問題下怎麼一堆只說 C 如何的,我感覺大家基本都離題了啊。

沒人列一下其他語言都是哪裡「被帶壞」了嗎。


想到兩篇經典文章

第一篇如果你用a tour of go來入門golang的話一開始就會看到,基本上是介紹go是怎麼擺脫c語言屎一樣的聲明方式以及為什麼還沾著一點屎…………

https://blog.golang.org/gos-declaration-syntax

第二篇非常古老,是當初lisp還在和c爭天下的時候lisp的人寫的,感慨c這種沒有完美主義信仰、爽了開發者坑了用戶的設計大行其道…………

http://dreamsongs.com/WIB.html


可能是因為原答案里感情色彩過於濃烈了,導致很多人都沒理解我的意思。改善了一下措辭,又加了寫內容

==========================

說一下我認為的 C 語言中帶來了很多問題,並且『帶壞』了很多語言的兩個設計:
1、switch 的貫穿
C 語言 switch 語句的貫穿問題也算是大名鼎鼎了吧。在 C 語言里,如果 switch 語句的一個 case 字句沒有通過 break 語句跳出,就會直接執行到下面一條 case 語句中,這個設計雖然也有它的用處,不過就統計結果來看,這種使用方式不如加上 break 的方式更常見,反而經常會因為忘記加上 break 產生問題。在很多較新的語言中都規避了這個問題,譬如放棄 C 類的 switch,採用更強大的模式匹配啊,或者是像 C# 一樣,用語法的限制來避免貫穿問題。

2、強制轉換語法

如果你不覺得這個語法有什麼不對的話,可以去看看Java的語言規範。涉及到語法歧義的討論基本上就少不了這個語法。

注意:我說的是C語言的強制轉換語法很蠢,而不是強制轉換本身很蠢

C里(type)expression 這樣的語法很容易產生歧義。Java繼承了C的強制轉換語法,所以我們能在規範中看到好幾處關於這個語法產生的歧義,或者是可能產生的歧義的討論。我們就舉個幾個例子吧。

譬如強制轉換語法會在類似 (p)+q 這樣的表達式中產生歧義。在Java語言規範(§15.15)中,有一段關於此歧義的論述:

The first potential ambiguity would arise in expressions such as (p)+q, which looks, to a C or C++ programmer, as though it could be either a cast to type p of a unary +operating on q, or a binary addition of two quantities p and q. In C and C++, the parser handles this problem by performing a limited amount of semantic analysis as it parses, so that it knows whether p is the name of a type or the name of a variable.

Java takes a different approach. The result of the + operator must be numeric, and all type names involved in casts on numeric values are known keywords. Thus, if p is a keyword naming a primitive type, then (p)+q can make sense only as a cast of a unary expression. However, if p is not a keyword naming a primitive type, then (p)+q can make sense only as a binary arithmetic operation. Similar remarks apply to the - operator. The grammar shown above splits CastExpression into two cases to make this distinction. The nonterminal UnaryExpression includes all unary operators, but the nonterminal UnaryExpressionNotPlusMinus excludes uses of all unary operators that could also be binary operators, which in Java are + and -.

The second potential ambiguity is that the expression (p)++ could, to a C or C++ programmer, appear to be either a postfix increment of a parenthesized expression or the beginning of a cast, for example, in (p)++q. As before, parsers for C and C++ know whether p is the name of a type or the name of a variable. But a parser using only one-token lookahead and no semantic analysis during the parse would not be able to tell, when ++ is the lookahead token, whether (p) should be considered a Primaryexpression or left alone for later consideration as part of a CastExpression.

In Java, the result of the ++ operator must be numeric, and all type names involved in casts on numeric values are known keywords. Thus, if p is a keyword naming a primitive type, then (p)++ can make sense only as a cast of a prefix increment expression, and there had better be an operand such as q following the ++. However, if p is not a keyword naming a primitive type, then (p)++ can make sense only as a postfix increment of p. Similar remarks apply to the -- operator. The nonterminal UnaryExpressionNotPlusMinus therefore also excludes uses of the prefix operators ++ and --.

可以看到,Java 用了一種很不優雅的方式解決了這個歧義。

而類型轉換的小括弧,又容易和 lambda 混淆。在 Java 語言規範(§15.27)里,就有這樣一段描述:

The syntax has some parsing challenges. The Java programming language has always required arbitrary lookahead to distinguish between types and expressions after a "(" token: what follows may be a cast or a parenthesized expression. This was made worse when generics reused the binary operators "&<" and "&>" in types. Lambda expressions introduce a new possibility: the tokens following "(" may describe a type, an expression, or a lambda parameter list. Some tokens immediately indicate a parameter list (annotations, final); in other cases there are certain patterns that must be interpreted as parameter lists (two names in a row, a "," not nested inside of "&<" and "&>"); and sometimes, the decision cannot be made until a "-&>" is encountered after a ")". The simplest way to think of how this might be efficiently parsed is with a state machine: each state represents a subset of possible interpretations (type, expression, or parameters), and when the machine transitions to a state in which the set is a singleton, the parser knows which case it is. This does not map very elegantly to a fixed-lookahead grammar, however. There is no special nullary form: a lambda expression with zero arguments is expressed as () -&> .... The obvious special-case syntax, -&> ..., does not work because it introduces an ambiguity between argument lists and casts: (x) -&> .... Lambda expressions cannot declare type parameters. While it would make sense semantically to do so, the natural syntax (preceding the parameter list with a type parameter list) introduces messy ambiguities. For example, consider:
foo( (x) &< y , z &> (w) -&> v )

This could be an invocation of foo with one argument (a generic lambda cast to type x), or it could be an invocation of foo with two arguments, both the results of comparisons, the second comparing z with a lambda expression. (Strictly speaking, a lambda expression is meaningless as an operand to the relational operator &>, but that is a tenuous assumption on which to build the grammar.) There is a precedent for ambiguity resolution involving casts, which essentially prohibits the use of - and + following a non-primitive cast (§15.15), but to extend that approach to generic lambdas would involve invasive changes to the grammar.

這幾個例子展現了強制轉換的語法帶來的問題,如果 Java 採用 D 語言,或者是 C++ 的強制轉換語法,就沒這麼多問題了。


c沒什麼問題,問題是教科書錯了。揪著錯誤不放。甚至 陷阱與缺陷 成了牛逼的流行。哥們兒從彙編開始學編程。那些錯誤,tmd半輩子就沒遇到過一次。我一直致力於用C寫出好的代碼。但也可以深刻理解golang等同父異母的語言存在的意義。大多數噴子根本沒有理解。只是從心理上覺得拍的是開心的。你完全可以去看linux的操作系統。有哪個語法是在溝里的?而往往那些二把刀水平。曾經寫過一些垃圾代碼。沾沾自喜的以會c語言自居。然後挑起一下爭端。

都洗洗睡吧


那畢竟是個從彙編改進的時代……有個基本的結構化編程的概念就不錯了。

而且當年也有比較美的語言,但是受限於硬體是沒可能發展的。

好和壞都是相對的,最終的目的都是滿足需求。需求變化了,方法和工具自然會發生變化。


個人認為C++就是個例子 比如std::string和const char *


怎麼我的編程熱情是被C給帶起來的呢,感覺好好。666


std縮寫(&,&

從C開始,標準庫和性病之間有了微妙的聯繫

帶壞了C++、D、rust


補充一點。

NULL。

NULL的萬惡之源不是C,但是C在NULL的傳播上做出了巨大貢獻。

關於為什麼NULL是evil的,可以參考NULL之父Tony Hoare本人的說法:Null References: The Billion Dollar Mistake

還有某些拿「語法糖」說事的,這種人我見得太多了,建議你們下次換個借口。


物競天擇,適者生存。

如果c真的很糟糕,那麼後來的人類就應該創建出一種新的語言來規避c的問題。

能被帶壞,說明本來就沒有好的。


看來很多人看不懂那麼簡單的答案那我就再舉個栗子好了。

先來看一段聲明

int foo;

眾所周知 foo 的類型是 int,但我們再來看一段聲明

int (*(*foo(int a))())();

這是個啥?(*(*foo(int))())() 的類型是 int?NO NO NO,是 foo(int) 的返回類型是 int (*(*)())()!

那麼為什麼會這麼聲明呢?因為 C 的聲明並不是「類型 標識符」而是「解 方程」。

(這裡請自行跳轉原答案)

-----------------------吐槽分割線(可不看)-----------------------

因為知乎上很多大 V 都說過 C 是解方程式聲明,所以我為了便於理解原答案就寫了「類型 表達式」,結果我不知道為什麼會有那麼多精英來質疑,首先 C 父都說了聲明與使用一致,其次樓上 Go 吐槽 C 的傳送門中用詞也是表達式,難道看我粉少好欺負?

還有我吐槽的的確是聲明,但我又沒說我不理解,並且第一句話的第一段就說了聲明格式,居然有人讓我去翻XX書XX節是什麼意思?

另外我說不要拿省資源說事明顯指的是總有一群人拿這當免死金牌無腦代入,就好比「他還只是個孩子」,結果一堆人說「這與省資源無關」「這與省資源無關」,EXO ME?我難道沒說嗎?復讀機?

=================以下原答案==================

感覺 C 最為詬病的就是以「類型 表達式」作為聲明形式,為此 * 放在哪邊爭論不休,函數指針類型聲明各種彆扭,而超集 C++ 的引用符號看起來格格不入但也只能默默吔下這口屎,連現在的編譯器報錯的時候都是 v 不符合類型 t *,而不是 *v 不符合類型 t,所幸現在的 C 系語言很少會採用這種形式了(?⊿?)?

另外這種聲明造成的最蛋疼的一個點就是,別的語言在解析一條語句的時候,第一節就代表這個語句的功能,但 C 非常雜,有聲明有定義有 struct 有 typedef,所以解析 C 語句的第一步卻是判斷是不是內置關鍵字先,間接導致了所有關鍵字包括變數名都在一個命名空間中,衝突起來很無語,然後聲明和定義你得解析完整句你才能知道吧,所以別說 C 那個年代要節省內存云云才這麼設計的,一個關鍵字能判斷的東西,你解析完整句才能判斷,到底哪個省資源就高下立判了吧?


100種語言里99種語言的虛擬機是c寫的,剩下的一種虛擬機的虛擬機還是c寫的。


在國內你會發現有好多帖子都在評價和對比諸多編程語言,其實很明顯這些編者肯定不是IT界的人,真正搞IT的從來不會把這些話題拿到桌面上討論,在真正的編程者心中他們都門兒清,並且知道在什麼場景下用什麼語言,畢竟造出一個完美的程序才是一個程序員的使命,誰好誰壞不值得重點區分討論


推薦閱讀:

如何高效的學習C++?
有哪些能炫技的代碼寫法?
如何寫優美的c++代碼?
為什麼C/C++相同內存布局的struct不能互相cast?
程序員應該將精力放在研究編程語言本身還是用編程語言創造好的軟體?

TAG:編程語言 | Java | C# | CC |