u/aghast_nj

466 post karma · 5,063 comment karma · joined Mar 2020

  1. 211points

    Reminder: Advent of Code starts December 1st (self.C_Programming) [nsfw]

    7 comments

  2. 57points

    Reminder: Advent of Code starts December 1st (self.cprogramming) [nsfw]

    1 comments

aghast_nj

There are three primary uses for pointers:

1- Passing modifiable parameters 2- Using dynamic memory 3- Passing compact references to read-only objects This may not be true, but it's almost true. (It was true on some old computers, long ago. And it's simple enough for you to understand.)

When you write a program, you specify code and data (set to some value other than zero) and other data (set to zero).

The compiler specifies a stack. So you have something like this:

==== code starts ====
...
==== code ends ====
==== data initialized to something starts ====
...
==== data initialized to something ends ====
==== data initialized to zero starts ====
...
==== data initialized to zero ends ====
==== stack starts ====
...
==== stack ends ====
==== BREAK ====

The OS loader will take your program that contains the CODE and DATA (initialized) and copy it into memory. The DATA (initialized to zero) part needs only a size: just set so-many bytes to zero. The STACK part needs only a size, and doesn't have to be set to zero.

The runtime library contains a function called sbrk(n) (set break). That function moves the "BREAK" pointer upward or downward by a parameter amount. If you call sbrk(100) the BREAK pointer moves away from your data by 100 bytes. If you call sbrk(-100) the BREAK pointer moves closer to your data by 100 bytes. (You should never pass a negative amount, unless you are really, really sure what you are doing.)

You don't call sbrk() very often. What happens instead is that other library functions, like malloc() call sbrk(). Let's leave it to them. The point is that there is somewhere that the C library can get "extra memory" from. This isn't magic, it's just the difference between the size of available memory (system RAM, in other words) and the size of your program that will need to run. This includes all those zero-filled DATA bytes, which aren't a part of the executable. This includes the stack, which isn't part of the executable. They're just little annotations somewhere in the early part of the executable record that indicate how much extra space will be needed.

So when you call malloc() it has to check it's own records, to see if any "extra memory" has been grabbed in advance (using sbrk()). If not, like if this is the very first time you call malloc(), then it requests some space. Usually, malloc requests a bunch of memory, like 64k. A big amount. Or, on a 64-bit machine, maybe it asks for like 4 gigs at a time. It's not your business, really, except that it wants to allocate a lot more than your first request.

So malloc has a data structure that holds the address of a big bunch of memory. What does it do? It adjusts a pointer to leave room for a "tracking" block, and makes sure the resulting pointer has the correct alignment (because malloc promises to return aligned memory) and returns that pointer to you.

So malloc() is a way for you, as a coder, to take control of memory that wasn't actually part of your program. How do you deal with that memory?

Well, you use a pointer. There is no other way for you to access the "extra" memory except by using the address of the memory, because that is all you get. There is no "name" (identifier) you can use. You have no idea where this memory will be. So you call malloc() and you get back a pointer value (an address in extra memory somewhere). So you store the returned result into a pointer, and check it for null:

int * my_array = malloc(100 * sizeof (int));

if (my_array == nullptr)
    FAIL("malloc 100 ints");

This is not the only way to use pointers,

Passing modifiable parameters

C does not support "references" like C++ does. Nor does it support any kind of mutable parameter. C supports "pass-by-value" parameters. This means that a copy of each parameter is made and stored on the stack or in a register, and that copy is not the original value, but is a separate copy.

This separate copy means that you can write foo(9 + 2) and have it work because the parameter is a copy of the incoming value. It also means that the callee function can make changes to the parameters, since they are a separate copy and there is no need to worry about changes.

But it means there is no way to directly change an incoming parameter. Unless you pass the address of where the incoming parameter is stored. If you do this, you can use the pointer to make changes to the original value:

void foo(int * x) {
    *x += 1;
}

You can obviously make changes to complex structures, etc., using this approach. Thus, making changes to parameters is the most obvious use of pointers.

Using dynamic memory

You can sometimes plan your code so that all the variable you will need are declared as part of the code when you write it. But the reality is that you will quickly find programs that require variably sized lists or arrays of structures. For example, a program that reads in a lists of test results may first read in the number of such test results.

This is the simplest version of dynamic memory. Other programs may have to parse a document (like an HTML page pulled from the internet) and break the document down into various "nodes". In this circumstance, you will be constructing some sort of data structure as you go along, a very dynamic data structure indeed.

There is no way you can predict what's happening. The best you can do is declare one variable: the "here is the root of my tree, or the start of my array" variable. But the actual data will be dynamic, so the "root" variable will be a pointer. There might be other pointers if you are parsing HTML. (An array is just a line of things all right beside one another, so no pointers required...)

Passing compact references to read-only objects

This is very similar to the modifiable-parameters case, above. The difference is that you are passing the pointer solely to avoid copying lots of data onto and off-of the stack. Usually, you declare the pointer const to signify the read-only nature:

void do_something(const ConfigData * cd) {...}

The point here is that you could have passed a ConfigData argument, but you didn't. The purpose was not to modify the ConfigData, but to save the time and energy required to slam things around on the stack. It's easier to move 4 or 8 bytes onto the stack (a pointer) than to copy 32 or 128 or whatever just to get data into a function.

aghast_nj

Is this ... No Net November?

aghast_nj

When your clever app has made you soooo wealthy that you find yourself buying a company that makes rocket ships.

Until then, you're just a mediocre programmer.

aghast_nj

That name though...

aghast_nj

No. I've never heard of this "technical rounds" concept.

(FWIW: I'm a USA-based coder, I have worked as a consultant for many, many USA-based shops across multiple industries: automotive, financial, insurance, medical, controls. If this was a "thing" I expect I would have heard of it, unless it's offshore or really new.)

aghast_nj

Why not use the existence of the /home/... folder as your determining fact? That makes it possible to write a $(shell) expression that directly expands to your path:

mypath=$(shell if [ -d /home/project/lib ] ; then echo /home/project/lib ; else echo /mnt/912345/project/lib ; fi )

You may reverse the condition if you like, checking for nonexistence (if [ ! -d) or checking for the mnt directory instead of /home. But your writing seems quite explicit about the existence of /home/... being an absolute determiner of things, so why not use that?

Anyway, you might want to add an abstraction layer, creating a "where am I" state variable that you can use to switch more than one setting. (Anything directory based, I guess.) Something like

location = $(shell ... ??? ... )
if location = at-home
    LIBPATH=/home/project/lib
    INCLUDEPATH=/home/project/include
    ARTPATH=/home/project/artwork
else if location = at-office
    PROJROOT=/mnt/8675309/project/
    LIBPATH=${PROJROOT}lib
    INCLUDEPATH=${PROJROOT}include
    ARTPATH=${PROJROOT}artwork
else ...
    ...
endif
aghast_nj

Re (Q1):

I note that you included no rules for bold-faced operators, only for bold operands. "If an operator such as + has bold face operands, then that operator denotes the computer's addition operation."

So look for what bold-operator is supposed to mean. Maybe it's unsigned, maybe it's allowing-for-type-expansions, maybe it's "theoretical math without regard to bit sizes".

Or maybe it's an error. The beginnings of books are chock full of errors, because (1) the author keeps rewriting them; and (2) nobody wants to proof read the beginning because it's full of boring "Hello and welcome" stuff that doesn't mean anything...

Re (Q2):

I think this is the normal computer operation. The rules you quoted say a plain old operator with bold operands is the computer version, right? So a 32-bit add of signed values will be done, and one most CPUs that will overflow, leaving zero in the register, carry/overflow bit set, etc.

aghast_nj

I don't understand the question. What is "technical rounds"? What is the context, here? Are you presuming that senior developers wander around some company quizzing junior developers? That's a huge waste of employee time, no manager would allow it!

Is this a college/university thing? Please give details.

aghast_nj

First, I suggest you separate the "fill the edges with border tiles" code from the rest. That conditional is pointless since you know exactly what will be done. Just do that thing, and adjust the start/end accordingly:

insert_border_tiles(0, 0, x, y);
insert_random_filler_tiles(1, 1, x-1, y-1);

(You probably want a "Point" or a "Range" or a "Rectangle" type for all these grouped integers...)

You may want to consider breaking your space down into separate areas. As soon as you start on this path, I recommend you look at "rogue-like" games, which do this in various ways (type "7drl" into your favorite search engine to get started, or see https://7drl.com/ ). For specific example, the very-old game "larn" used a single field (60-ish by 20-ish) and partitioned it by either loading hand-drawn levels, or generating a maze with rooms that used the field rectangle perfectly. (It generated a maze, then dropped rooms in. It was clever, but not magical.)

aghast_nj

Yes, I do. C is described by many as a "bare bones" language, and I think that is a strength in this context. Learning to program in C means learning to think about everything. If I were designing a "programme" for a university, I would take the opportunity to learn about different ways of error handling, different ways of program structure, etc.

That means using C and trying to add on other features, then using some other languages and contrasting them back with the core C language and seeing what was easier to do, what harder.

Essentially, all those language that are "like C but with ..." need to be contrasted against C. So, do that thing. That would cover Golang, Rust, Pascal/Modula, Ada, Zig, C++, etc.

aghast_nj

Null pointers, which are codified in the C standard as being zero, are a concept more than anything else.

The idea is that there is this "magic value" that means "I don't point to any valid thing." That's the null pointer. It points to "null" or "nothing". By convention that is a pointer with value 0x00...00.

Strangely enough, this produces two effects: first, the C compiler, and associated tools like the linkers and library files and loaders and the standard C library itself go out of their way to ensure that a zero pointer is never created or used, except when it makes sense to signal an invalid pointer (like if a function-returning-pointer wants to indicate failure); and second, other parts of the operating system and runtime library sometimes conspire to make it really hard to access that range of addresses.

However, especially in the embedded world, there are plenty of systems where accessing memory at or close-by location zero makes sense.

For example, there are systems that store their interrupt vector tables in the zero page, or in the -1 page (just before zero, wrapped around). There are systems where memory in the zero page is faster and so used as high-performance local variables. There are tiny systems where the only RAM that exists at all is in the zero page.

In general, systems that are NOT "virtual memory" systems tend to take advantage of the "distinct" locations, including the zero page. Virtual memory systems, of course, are virtual -- they can do whatever they want with addresses.

A compiler that is aware of your target system should also be aware of whatever strange conventions surround your target system. So a target that uses the zero page for memory should have some mechanism for that. A target that uses the zero page for interrupt vectors should have some mechanism for that.

In the simplest case, I suggest being "a little stand-offish" about the whole thing. Write functions to access locations close to zero in a way that makes sense, like void set_interrupt_vector(int irq_num, void (*handler)()) and maybe a getter. Then you can code those two functions in a separate file, carefully examine the generated assembly using something like the -S flag, and move on.

If you are stuck with a compiler that "expects" your system to be a VM system even when it isn't, you may have to work around it. The easiest way it probably to do everything indirectly by taking a pointer to an array, or a pointer to a struct, and just either passing zero or using a global zero variable as the pointer.

aghast_nj

If you think about it, you will realize that the solution to any "wastage" problems is to place each entry-point function into its own separate source file.

Strangely, this is exactly what almost every libc package does -- separate files for each entry point function. (Note that if, say, printf() had a "private" function that it called for formatting real numbers or something, that function would (a) probably be labelled static; and (b) be perfectly fine, even if not static, contained in the same source file as printf since it is only ever called from there.)

I encourage you to take a look at the musl or gnu libc sources, both of which I am sure are available on github, possibly hundreds of times. See what organization they use, what tricks of linking and other symbol management are going on. Those libraries are under a lot of pressure from various systems - not merely Linux - so you can bet that they are as "middle of the road" as possible in terms of using special features. But they can do a whole lot of cool stuff with just the middle of the road features present in "every" system.

aghast_nj

If you pass every function a pointer to the thing, then you never have to worry about changes you make to the thing.

"What if I change it from a global variable to a local variable set up in main()?" Nobody cares, here's a pointer.

"What if I dynamically allocate the variable on the heap using malloc()?" Nobody cares, here's a pointer.

"What if I rearrange all the fields and sort them alphabetically by name?" You're a dumbass, nobody cares, here's a pointer.

"What if I convert the game to multiplayer using an array of game structures?" Nobody cares, here's a pointer.

The only change you might make that would have an impact would be to switch from a single struct {} to a bunch of parallel fields, like an SOA/AOS change. And that would presume a big change in your game, from text based single user to massively multiplayer or something. In which case you will be glad to pay the price, for whatever reasons.

Technically, this is called "decoupling". You are removing the coupling between how the variable is implemented (local, global, allocated on heap, etc) and how the various functions get called with it. A pointer does that job nicely, and will be instantly familiar to almost every C programmer: "Oh, here's a pointer to all the data I'll need. Nice!"

One thing for you to worry about: different structs. You may have a Player struct and a Scoreboard and a Map and a GameLevel and a Monster, etc. You'll have to decide which of those structs contain pointers to which other structs, and pass the appropriate things around. Good luck!

aghast_nj

... If you build it, they will come?

aghast_nj

There are things the hardware can do, and things it cannot do. For example, the x86/x64 architecture has two registers oriented at maintaining the stack and the stack frame. As a result, that architecture is really good at putting things on the stack.

By contrast, SPARC hardware had big files of registers and a mechanism for a "register window" that meant the stack frame was usually all stored in registers and there was nothing "on the stack" most of the time. (There technically was a stack, but using it meant you were passing way too many parameters, etc.)

By contrast, the 6502 architecture has 3 registers (A, X, Y) and in many implementations a "zero page" of "fast" memory locations (basically addresses 0x0000 through 0x0100 are special). There's very little hope of passing much information in registers, so everything either goes into fast memory or on the stack.

So right from the bare metal, there are going to be biases built in to the ABI for that particular bare metal. Then the OS implementor will provide extra constraints: calls from user programs to the OS are made by ... generating a particular interrupt; or executing a special instruction; or jumping to a certain "magic" address. So the OS will take the capabilities of the hardware and extend them a little to cover decisions made about communications between process and OS.

Finally, the programming language comes along and specifies how the details of that particular language will be implemented. For the C programming language, function calls are important. Parameter passing is important. How to pass varargs parameters is important. How to implement setjmp/longjmp is important. So each compiler needs to work out how to do this.

But if compilers want to interoperate, it's better if their compiler designer comes to a shared agreement with other compiler designers, so that the Clang compiler and the Intel compiler and the IBM compiler and the GCC compiler all generate function calls that work the same way, with parameters in the same registers or the same places on stack, etc. Instead of having a "ABC Corp" ABI for the C programming language, it's better to have a "combined ABI" that all the designers agree with and set their compiler(s) to generate code for.

Note that sometimes the C ABI includes weird functions, because the OS or the hardware may not provide enough support. (For example, really old SPARC systems did not have multiply instructions. There was a "partial multiply" that could be chained together to do multiplication. So a compiler could chain up a bunch of instructions, or make a call to a BIOS routine, or make an OS call ... to multiply.)

Note that the ABI will be programming language specific. C++ has exceptions, while C does not. C++ has virtual functions while C does not. Other languages make guarantees about array accesses being bounds checked (C does not, but you knew this was coming, right?).

Lots of code is written in C. Lots of other languages want to be able to call into and out of C. So it makes sense to have a C ABI developed early, and published for everyone to read. Other languages can wait a bit, or build on top of this with their extra features. (It may be possible to write code in Fortran and call it from PL/I. But who would want to?)

aghast_nj

Is that the Phillipines? Too-low phone/electric wires, teal striped license plates, Watsons pharmacy...

aghast_nj

The code looks good. The hour-long requirement doesn't seem excessive for the first time you encounter 2-d arrays, and I would wager that your next problem will take much less time to solve, now that you have the hang of it.

aghast_nj

Also, I suggest that you move away from 1=yes, 0=no for boolean questions, and write a function to ask yes/no questions for you and return a boolean result:

if (ask_yn("Is there a son?")) { ... }

aghast_nj

I know absolutely nothing about the subject of Islamic family inheritance, or even nomenclature.

In your InputChildren function you ask "Is there a son?" and then ask "How many daughters?"

But what if there are multiple male children? Does a second or third son simply not inherit anything?

It seems like you have the storage allocated for more than one male child. But I don't know what the rules are, or whether it makes any sense to ask these questions...

aghast_nj

Memory that you get from the heap will live until you tell it to die. (malloc ... free)

char * a; // global variables are bad, m'kay?
void fn1() { a = malloc(100); }  // buffer a lives!
void fn2() { free(a); a = NULL; }    // buffer a dies!
main(void) {
    fn1();    // memory allocated in here lives ...
    fn2();    // ... until I destroy it in here
}

Memory on the stack lives until the lexically containing function exits, then dies.

void foo() {
    char a[20]; // buffer a lives!
    do_something_with(a);
    do_something_else_with(a);
}    // buffer a dies!

If you have an "endless loop" in your function (like the event loop for a GUI or a video game) your stack variables will live almost forever. But as soon as you exit the enclosing function, they go away.

aghast_nj

First, consider how you drew your parentheses:

(x) (=) (i++); 

What could that possibly mean? (x) is okay, that's just x. But (=)? What would that be?

In general, when you are breaking down precedence using parentheses, put the operator and any operands in the same set of parens. You may wrap identifers (variable or function names) in parens with no operator first, if you like (but it generally doesn't help):

x = i++
x = (i)++
x = ((i)++)
(x) = ((i)++)
((x) = ((i)++)

The parens around i and x don't help much, so:

x = i++
x = (i++)        // do the postincrement first
(x = (i++)      // then the assignment

The standard says:

6.5.2.4 Postfix increment and decrement operators

Constraints
1 The operand of the postfix increment or decrement operator shall have atomic, qualified, or unqualified real or pointer type, and shall be a modifiable lvalue.

Semantics
2 The result of the postfix ++ operator is the value of the operand. As a side effect, the value of the operand object is incremented (that is, the value 1 of the appropriate type is added to it).

Note here that the result of the operator is clearly stated. The increment is a side-effect. So, if you have i = 9; and then evaluate i++ the result is going to be 9. It says so clearly. As a side-effect, the value of i will be incremented. But it's too late for that -- the result of the operator will be "the value of the operand".

So if you do:

int x = 0;
int i = 9;

x = i++;

What is x? It's 9.

What is i? Well, that depends. Eventually an increment will take place and it will be 10. But the compiler is free to kick that can down the road a ways, if it wants to.

aghast_nj

There are a bunch of requirements for game state save files. None of them are technical requirements, though.

Is your game a commercial product? Will you have to provide "tech support" for this?

Is there an advantage to be had in "editing" the state files? Will you be losing money, or damaging your market, if people are able to do so? (A textual state file is great for reading, post-processing, verification, etc. But it is vulnerable to editing, so if editing is bad, don't do text.)

Is your game still being modified? Is there a reason to think a change might come that needs more data in the save file? Will your file format be able to smoothly upgrade in the next version?

Does your savegame data format need version info? Will someone be able to save a game, come back in 18 months and load okay?

Will you need to support saving 3rd party plugins state as well? What about 'mods'?

aghast_nj

Be careful. First, Visual Studio is mainly a C++ oriented product. So things that might work in VS may not work when you start compiling "strict mode" C. Second, this is something added to C23. So if a course, or a product you are using, says "C99" or "C89" or even "C11", none of those is C23 and so this will not work.