156

Currently C is considered a low level language, but back in the 70's was it considered low level? Was the term even in use then?

Many popular higher level languages didn't exist until the mid 80's and beyond so I'm curious if and how the nature of low level has changed over the years.

9
  • 15
    As one data point, circa 1978 some programming mentors described C to me as a glorified assembly language. Jul 1, 2018 at 17:56
  • 7
    @BenCrowell I am not sure what “glorified” means in the context of your statement but I’ve experienced calling C a universal (= platform-independent) assembly language.
    – Melebius
    Jul 2, 2018 at 9:56
  • 14
    Interestingly, there's a case to be made that C Is Not a Low-level Language, and is less one now than it was in the 70s, because C's abstract machine is farther removed from modern hardware than it was from the PDP-11.
    – Tom
    Jul 2, 2018 at 19:46
  • 5
    When thinking of this you might also want to think of the origin--Unix is written in C, c runs on Unix and cross-compiles unix to other platforms. Anything not necessary to compile Unix was unnecessary overhead, and C's main goal was to make it as easy to write/port a compiler as possible. For this purpose it was the EXACT correct level, so I don't think of C as high or low level, I think of it as a tool to port Unix that, like nearly all Unix tools, is extremely adaptable to many problems.
    – Bill K
    Jul 2, 2018 at 21:56
  • 4
    It's worth noting that lisp was invented in 1958
    – Prime
    Jul 3, 2018 at 17:42

7 Answers 7

147

To answer the historical aspects of the question:

The design philosophy is explained in The C Programming Language written by Brian Kernighan and C designer Dennis Ritchie, the "K&R" you may have heard of. The preface to the first edition says

C is not a "very high level" language, nor a "big" one...

and the introduction says

C is a relatively "low level" language... C provides no operations to deal directly with composite objects such as character strings, sets, lists, or arrays. There are no operations that manipulate an entire array or string...

The list goes on for a while before the text continues:

Although the absence of some of these features may seem like a grave deficiency,... keeping the language down to modest size has real benefits.

(I only have the second edition from 1988, but the comment below indicates that the quoted text is the same in the 1978 first edition.)

So, yes, the terms "high level" and "low level" were in use back then, but C was designed to fall somewhere on the spectrum in between. It was possible to write code in C that was portable across hardware platforms, and that was the main criteria for whether a language was considered high level at the time. However, C lacked some features that were characteristic of high level languages, and this was a design decision in favor of simplicity.

1
  • 42
    This is an excellent answer ! Verifiable historical evidence + testimony of an actor best informed of the meaning of low and high level in the period the OP refers to. By the way, I confirm that the quotes were already in the 1978 edition.
    – Christophe
    Jul 1, 2018 at 16:42
157

This depends on your definition of high-level and low-level language. When C was developed, anything that was higher-level than assembly was considered a high-level language. That is a low bar to clear. Later, this terminology shifted to the point that some would nowadays consider even Java to be a low-level language.

Even within the high-level language landscape of the 70s, it is worth pointing out that C is fairly low level. The C language is basically B plus a simple type system, and B is not much more than a convenient procedural/structured syntax layer for assembly. Because the type system is a retro-fit on top of the untyped B language, you can still leave out type annotations in some places and int will be assumed.

C consciously leaves out expensive or difficult to implement features that were already well-established at the time, such as

  • automatic memory management
  • nested functions or closures
  • basic OOP or coroutines
  • more expressive type systems (e.g. range-restricted types, user-defined types such as record types, strong typing, …)

C does have some interesting features:

  • support for recursion (as a consequence of its stack-based automatic variables, as compared to languages where all variables have global lifetime)
  • function pointers
  • User-defined data types (structs and unions) were implemented shortly after C's initial release.
  • C's string representation (pointer-to-chars) is actually a huge improvement over B which encoded multiple letters into one machine word.
  • C's header files were an efficiency hack to keep compilation units small, but also happen to provide a simple module system.
  • Assembly-style unrestricted pointers and pointer arithmetic, as compared to safer references. Pointers are an inherently unsafe feature but also very useful for low-level programming.

At the time when C was developed, other innovative languages such as COBOL, Lisp, ALGOL (in various dialects), PL/I, SNOBOL, Simula, and Pascal had already been published and/or were in wide use for specific problem domains. But most of those existing languages were intended for mainframe programming, or were academic research projects. E.g. when ALGOL-60 was first designed as an universal programming language, the necessary technology and computer science to implement it didn't exist yet. Some of these (some ALGOL dialects, PL/I, Pascal) were also intended for low-level programming, but they tended to have more complex compilers or were too safe (e.g. no unrestricted pointers). Pascal notably lacks good support for variable-length arrays.

Compared to those languages, C rejects “elegant” and expensive features in order to be more practical for low-level development. C was never primarily a language design research project. Instead, it was an offshoot of Unix kernel development on the PDP-11 minicomputer which was comparatively resource-constrained. For its niche (a minimalist low-level language for writing Unix with a single-pass compiler that's easy to port) C absolutely excelled – and over 45 years later it still is the lingua franca of systems programming.

23
  • 17
    Just a small addition for people who are unaware of what was necessary for the existing ALGOL family and Pascal languages: Those languages had lexically nested functions where you could access (local) variable declared in outer functions. That meant either that you had to maintain a "display" - an array of pointers to outer lexical scopes - at each function call and return (that changed lexical level) - or you had to chain lexical scopes up the stack and each such variable access required multiple indirect hops up the stack to find it. Expensive! C jettisoned all that. I still miss it.
    – davidbak
    Jul 1, 2018 at 5:42
  • 12
    @davidbak: The x86-64 System V ABI (aka the calling convention on Linux / OS X) defines %r10 as the "static chain pointer", which is exactly what you're talking about. For C it's just another call-clobbered scratch register, but I guess Pascal would use it. (GNU C nested functions use it for passing a pointer to the outer scope when such a function doesn't inline (e.g. if you make a function pointer to it so the compiler creates a trampoline of machine code on the stack): Acceptability of regular usage of r10 and r11) Jul 1, 2018 at 8:54
  • 2
    @PeterCordes Fascinating! Pascal was still widely used when System V came out (though I don't know when the formal SysV ABI was defined). Your linked answer is very informative.
    – davidbak
    Jul 1, 2018 at 16:03
  • 3
    C has user-defined types (struct/union). The rest of those "features" were left out, I suspect, because they are of zero or negative use (unless you are entering an obfuscated code contest :-)), as they detract from the goal of keeping the language both simple and expressive.
    – jamesqf
    Jul 1, 2018 at 17:25
  • 6
    @jamesqf: but very early C didn't have struct assignment; I guess you have to memcpy or copy the members individually instead of writing a = b; to copy a whole struct the way you can in ISO C89. So in early C, user-defined types were definitely second-class and could only be passed by reference as function args. C's aversion to arrays and also Why does C++ support memberwise assignment of arrays within structs, but not generally? Jul 1, 2018 at 22:17
38

In the early 1970s, C was a dazzling breath of fresh air using modern constructs so effectively that the entire UNIX system could be rewritten from assembly language into C with negligible space or performance penalty. At the time many contemporaries referred to it as a high level language.

The authors of C, primarily Dennis Ritchie, were more circumspect and in the Bell System Technical Journal article said "C is not a very high-level language." With a wry smile and intending to be provocative, Dennis Ritchie would say it was a low-level language. Chief among his design goals for C was to keep the language close to the machine yet provide portability, that is machine independence.

For more info consult the original BSTJ article:

Thank you Dennis. May you rest in peace.

6
  • 3
    It was basically typed and nicer-syntax wrappers for PDP assembly if you ask me.
    – einpoklum
    Jun 30, 2018 at 22:43
  • 2
  • 2
    @einpoklum I tend to agree with that. I learned C back in about 1980 at university, on a PDP-11/34a as it happens, and it was described by one of the profs as a "Portable assembly language." Probably because in addition to the PDP-11, we had several Superbrain CP/M machines in the lab that had a C compilers available on them. en.wikipedia.org/wiki/Intertec_Superbrain
    – dgnuff
    Jul 1, 2018 at 8:09
  • 2
    Also an excellent answer based on verifiable historical evidence (wow! there was a pre-version of the K&R available out there!).
    – Christophe
    Jul 1, 2018 at 16:47
  • 7
    @einpoklum Isn't this somewhat far-fetched ? I had the opportunity to write system and application code in the mid 80's in assembler (Z80 and M68K), a "portable assembler" called M (PDP11 syntax with an abstract 16 bit register and instruction set), and C. In comparison to assembler, C is definitively a high level language: the productivity of C programming was an order of magnitude higher than in assembler! Of course no strings (SNOBOL), no native datafile support (COBOL), no self generating code (LISP), no advanced math (APL), no objects (SIMULA), so we can agree that it was not very high
    – Christophe
    Jul 1, 2018 at 17:02
21

As I wrote elsewhere on this site when someone referred to the malloc/free memory management pattern as "low-level programming,"

Funny how the definition of "low-level" changes over time. When I was first learning to program, any language that provided a standardized heap model that makes a simple allocate/free pattern possible was considered high-level indeed. In low-level programming, you'd have to keep track of the memory yourself, (not the allocations, but the memory locations themselves!), or write your own heap allocator if you were feeling really fancy.

For context, this was in the early 90s, well after C came out.

16
  • Wasn't the standard library only a thing since standardization in the end-80s (well, based on existing Unix APIs)? Also, Kernel programming (which was C's original problem domain) naturally requires low-level stuff like manual memory management. At the time C must have been the highest level programming language that a serious kernel was written in (I think nowadays the NT kernel uses a fair amount of C++ as well).
    – amon
    Jun 30, 2018 at 16:36
  • 6
    @hyde, I used Algol 60, two different FORTRAN dialects, and several different BASIC dialects back in the 1970s, and none of those languages had pointers or a heap allocator. Jun 30, 2018 at 23:49
  • 7
    Strictly speaking, you can still program with no malloc() by directly calling brk(2) or mmap(2) and managing the resulting memory yourself. It's a massive PITA for no conceivable benefit (unless you happen to be implementing a malloc-like thing), but you can do it.
    – Kevin
    Jul 1, 2018 at 3:45
  • 1
    @amon - except for the remarkable Burroughs stack-based machines that were programmed in ALGOL from bottom to top...and much earlier than Unix, too. Oh, and by the way, Multics, which was the inspiration for Unix: Written in PL/I. Similar to ALGOL, higher level than C.
    – davidbak
    Jul 1, 2018 at 5:51
  • 6
    Actually, the reason more of us aren't using Multics today probably has more to do with the fact that it only ran on hellishly expensive mainframes - i.e., the typical outrageous mainframe expense of the day plus the extra expense of a half a cabinet of specialized hardware to implement virtual memory with security. When 32-bit minicomputers like the VAX-11 came out everyone but the banks and the government decamped from IBM and the Seven Dwarfs and took their large-scale processing to "mini" computers.
    – davidbak
    Jul 1, 2018 at 22:37
15

Many answers have already referred to early articles that said things like “C is not a high level language”.

I can’t resist piling on, however: many, if not most or all HLLs at the time - Algol, Algol-60, PL/1, Pascal - provided array bounds checking and numeric overflow detection.

Last I checked buffer and integer overflows were the root cause of many security vulnerabilities. ... Yep, still the case...

The situation for dynamic memory management was more complicated, but still, C style malloc/free was a great step backward in terms of security.

So if your definition of HLL includes “automatically prevents many low level bugs”, well, the sorry state of cybersecurity would be very different, probably better, if C and UNIX had not happened.

18
  • 7
    Since I happen to have been involved in the implementation of Intel MPX and the pointer/bounds checking compiler that spun out if it, I can refer you to the papers on their performance: essentially 5-15%. Much of that involves compiler analyses that were barely possible in the 1970s and 1980s - compared to naive checks that might be 50% slower. However, I think that it is fair to say that C and UNIX set back work on such analyses by 20 years - when C became the most popular programming language, there was much less demand for safety.
    – Krazy Glew
    Jul 1, 2018 at 17:45
  • 9
    @jamesqf Moreover, many machines prior to C had special hardware support for bounds checking and integer overflows. Since C did not use that HW, it eventually was deprecated and removed.
    – Krazy Glew
    Jul 1, 2018 at 17:48
  • 6
    @jamesqf For example: the MIPS RISC ISA originally was based on the Stanford benchmarks, which were originally written in Pascal, only later moved to C. Since Pascal checked for signed integer overflow, therefore so did MIPS, in instructions like ADD. Circa 2010 I was working at MIPS, my boss wanted to remove unused instructions in MIPSr6, and studies showed that the overflow checks were almost never used. But it turned out that Javascript did such checks - but could not use the cheap instructions because of lack of OS support.
    – Krazy Glew
    Jul 1, 2018 at 17:56
  • 3
    @KrazyGlew - It's very interesting that you bring this up since in C/C++ the fight between users and compiler writers over "undefined behavior" due to signed integer overflow is heating up, since todays compiler writers have taken the old mantra "Making a wrong program worse is no sin" and turned it up to 11. Plenty of posts on stackoverflow and elsewhere reflect this...
    – davidbak
    Jul 1, 2018 at 22:45
  • 2
    If Rust had SIMD intrinsics like C, it might be a nearly ideal modern portable assembly language. C is getting worse and worse as a portable assembly language because of aggressive UB-based optimization, and failure to portably expose new primitive operations that modern CPUs support (like popcnt, count leading/trailing zeros, bit-reverse, byte-reverse, saturating math). Getting C compilers to make efficient asm for these on CPUs with HW support often requires non-portable intrinsics. Having the compiler emulate popcnt on targets without it is better than idiom-recognition to get popcnt. Jul 1, 2018 at 22:59
8

Consider older and much higher languages that predated C (1972):

Fortran - 1957 (not much higher level than C)

Lisp - 1958

Cobol - 1959

Fortran IV - 1961 (not much higher level than C)

PL/1 - 1964

APL - 1966

Plus a mid level language like RPG (1959), mostly a programming language to replace plugboard based unit record systems.

From this perspective, C seemed like a very low level language, only a bit above the macro assemblers used on mainframes at the time. In the case of IBM mainframes, assembler macros were used for database access such as BDAM (basic disk access method), since the database interfaces hadn't been ported to Cobol (at that time) resulting in a legacy of a mix of assembly and Cobol programs still in use today on IBM mainframes.

9
  • 2
    If you want to list older, higher languages, don't forget LISP. Jul 2, 2018 at 21:53
  • @Deduplicator - I added it to the list. I was focusing on what was used on IBM mainframes, and I don't recall LISP being that popular, but APL was also a niche language for IBM mainframes (via time sharing consoles) and IBM 1130. Similar to LISP, APL is one of the more unique high level languages, for example look how little code it takes to create Conway's game of life with a current version of APL: youtube.com/watch?v=a9xAKttWgP4 .
    – rcgldr
    Jul 3, 2018 at 8:00
  • 2
    I wouldn't count RPG or COBOL as particularly high-level, at least not prior to COBOL-85. When you scratch the surface of COBOL, you see that it is essentially a collection of very advanced assembler macros. To begin with, it lacks both functions, procedures and recursion, as well as any kind of scoping. All storage must declared at the top of the program leading to either extremely long overtures or painful variable reuse.
    – idrougge
    Jul 3, 2018 at 11:25
  • I have some bad memories of using Fortran IV, I don't recall it being appreciably "higher level" than C.
    – DaveG
    Jul 3, 2018 at 13:54
  • @idrougge - COBOL and RPG, and also mainframe instructions sets included full support for packed or unpacked BCD, a requirement for financial software in countries like the USA. I consider the related native operators such as "move corresponding" to be high level. RPG was unusual in that you specified linkage between raw input fields and formatted output fields and/or accumulators, but not the order of operations, similar to the plugboard programming it replaced.
    – rcgldr
    Jul 3, 2018 at 20:53
6

The answer to your question depends upon which C language it is asking about.

The language described in Dennis Ritchie's 1974 C Reference Manual was a low-level language which offered some of the programming convenience of higher-level languages. Dialects derived from that language likewise tended to be low-level programming languages.

When the 1989/1990 C Standard was published, however, it did not describe the low-level language which had become popular for programming actual machines, but instead described a higher-level language which could be--but was not required to be--implemented in lower-level terms.

As the authors of the C Standard note, one of the things that made the language useful was that many implementations could be treated as high-level assemblers. Because C was also used as an alternative to other high-level languages, and because many applications didn't require the ability to do things that high-level languages couldn't do, the authors of the Standard allowed implementations to behave in arbitrary fashion if programs tried to use low-level constructs. Consequently, the language described by the C Standard has never been a low-level programming language.

To understand this distinction, consider how Ritchie's Language and C89 would view the code snippet:

struct foo { int x,y; float z; } *p;
...
p[3].y+=1;

on a platform where "char" is 8 bits, "int" is 16 bits big-endian, "float" is 32 bits, and structures have no special padding or alignment requirements so the size of "struct foo" is 8 bytes.

On Ritchie's Language, the behavior of the last statement would take the address stored in "p", add 3*8+2 [i.e. 26] bytes to it, and fetch a 16-bit value from the bytes at that address and the next, add one to that value, and then write back that 16 bit value to the same two bytes. The behavior would be defined as acting upon the 26th and 27th bytes following the one at address p without regard for what kind of object was stored there.

In the language defined by the C Standard, in the event that *p identifies an element of a "struct foo[]" which is followed by at least three more complete elements of that type, the last statement would add one to member y of the third element after *p. Behavior would not be defined by the Standard under any other circumstances.

Ritchie's language was a low-level programming language because, while it allowed a programmer to use abstractions like arrays and structures when convenient, it defined behavior in terms of the underlying layout of objects in memory. By contrast, the language described by C89 and later standards defines things in terms of a higher-level abstraction, and only defines the behavior of code that is consistent with that. Quality implementations suitable for low-level programming will behave usefully in more cases than mandated by the Standard, but there's no "official" document specifying what an implementation must do to be suitable for such purposes.

The C language invented by Dennis Ritchie is thus a low-level language, and was recognized as such. The language invented by the C Standards Committee, however, has never been a low-level language in the absence of implementation-provided guarantees that go beyond the Standard's mandates.

2
  • This seems to say the same language is both high- and low-level depending on what the documentation says.
    – user253751
    Nov 5, 2020 at 9:16
  • @user253751: I suppose whether one uses the terms "high-level" and "low-level" to describe what languages have or what they lack. Sorta like the question of whether a baritone is a bass who can sing high notes, or a tenor who can't.
    – supercat
    Nov 5, 2020 at 15:39

Not the answer you're looking for? Browse other questions tagged or ask your own question.