17

I believe I have a good understanding of what is stored on the stack(for example primitive data types) and what's stored on the heap(for example an instance of a class. But is there anyway to verify this for educational/verification of my expectations reasons?

It can be in a debugger(gdb or rad debugger or worst case vs) or directly code based(thinking something like gettype(variable) but like isstackstored(variable).

you are viewing a single comment's thread
view the rest of the comments
[-] Lee@retrolemmy.com 2 points 1 day ago* (last edited 1 day ago)

but I’m still trusting that author. I was curious if I could prove it, or disapprove it on any system.

I understand and admire this. Don't trust me. Granted, at some point you're going to have to trust someone. I'm going to assume that you trust that a stack allocated variable will be referenced by way of the stack pointer. On x86, that's a register called "RSP" and I'll show you exactly how you can see for yourself what gcc/g++ is outputting.

Here's some code: test.cpp

#include <iostream>

using namespace std;

int main(void) {
  int x;
  x = 42;
  cout << "x = " << x;
  return 0;
}

You can have g++ convert this to assembly with comments with: g++ -S -fverbose-asm test.s test.cpp Here's a portion of the output on my system:

main:
.LFB1988:
	.cfi_startproc
	endbr64	
	pushq	%rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp	#,
	.cfi_def_cfa_register 6
	subq	$16, %rsp	#,
# test.cpp:4:   int x = 42;
	movl	$42, -4(%rbp)	#, x
# test.cpp:5:   std::cout << "x = 42";

The piece I'll point to is the references to the x variable:

# test.cpp:4:   int x = 42;
	movl	$42, -4(%rbp)	#, x

This is moving the constant 42 ($42) in to the memory location -4(%rbp). This is a way of basically saying "the value stored in %rbp minus 4". I did say %rsp is the stack pointer, so why is it referencing %rbp? Well, see a couple lines above: movq %rsp, %rbp #, (it's copying the value of %rsp in to %rbp and then referencing via %rbp, so %rbp is identical to what %rsp was before the extra variable allocations at the time of the assignment of x = 42). Also note in later examples how the offset (-4 in this example) to %rbp (and %rsp) changes as we add more variables.

OK so how can we actually see the value of %rsp (or %rbp)? Well with some inline assembler to copy the value of %rsp in to a variable and then print it. So here's an example printing x, pStack, and their addresses:

#include <iostream>

using namespace std;

int main(void) {
  int x;
  x = 42;
  int *pStack;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  __asm__("movq %%rsp, %0;" : "=m" (pStack));
  cout << "pStack = " << pStack << endl;
  cout << "&pStack = " << &pStack << endl;
  return 0;
}

On my system, here's the output I get when I run it:

$ ./test2
x = 42
&x = 0x7ffcbd78a0bc
pStack = 0x7ffcbd78a0b0
&pStack = 0x7ffcbd78a0c0

So you can see pStack is address 0x7ffcbd78a0b0 and x is address 0x7ffcbd78a0bc. Try this with more complex types (struct, class) and see that the variable addresses are lower than the stack pointer.

Now for a heap allocation:

test3.cpp
#include <iostream>

using namespace std;

int main(void) {
  int *x = new int;
  *x = 42;
  int *pStack;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  __asm__("movq %%rsp, %0;" : "=m" (pStack));
  cout << "pStack = " << pStack << endl;
  cout << "&pStack = " << &pStack << endl;
  return 0;
}

When I run it, I get:

$ ./test3
x = 0x5ca98bc312b0
&x = 0x7ffd768137c8
pStack = 0x7ffd768137c0
&pStack = 0x7ffd768137d0

So we can see that x (a pointer) is pointing to a drastically different memory address (it's the heap). So why is &x (address of x) the stack? Well because x itself is a stack variable, but it's a pointer that is pointing to heap allocated memory.

You can do this kind of test with your custom classes, fixed sized arrays like int x[10] or even with arrays of complex types BetterDouble bd[10] and so on until you're convinced.

My only way of verifying this that I can find is verifying copy. Basically make a variable and set it to something, make second variable and set it equal to first, modify second variable, test if variable 1 == variable 2. If yes, it’s heap allocated, if not it’s stack allocated. My understanding is no matter what you create, a value is created on the stack. The difference is if stack allocated the data is stored directly, if heap the data on the stack is a pointer to an address on the heap. As such if you copy the variable, your copying the value in the stack. If it’s stack allocated the data is literally copied and you have the same data in two places, when you modify one the other is left alone. If it’s heap allocated, the address is copied into a new spot on the stack, but since both point to same address if you modify one you modify both.

This may just be a terminology difference or I'm misreading, but I think you may be confusing pointers vs the data they point to and stack vs heap. The pointers and stack/heap are 2 separate, although somewhat related, concepts. What I mean is that the behavior you describe (modifying variable2 and having it impact variable1) is true if variable2 is a pointer to variable1 and you're modifying the data contained (what variable2 is pointing to), but that is independent of stack vs heap. Generally you reference heap allocated variables via pointers, but you can also reference stack allocated variables by pointer. Here's an example using only stack variables (and pointers).

#include <iostream>

using namespace std;

int main(void) {
  int x = 42;
  int *y = &x;
  int *pStack;
  __asm__("movq %%rsp, %0;" : "=m" (pStack));
  cout << "pStack = " << pStack << endl;
  cout << "&pStack = " << &pStack << endl;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  cout << "y = " << y << endl;
  cout << "*y = " << *y << endl;
  (*y)++;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  cout << "y = " << y << endl;
  cout << "*y = " << *y << endl;
  return 0;
}

Output:

$ ./test4
pStack = 0x7ffd87bbb440
&pStack = 0x7ffd87bbb448
x = 42
&x = 0x7ffd87bbb444
y = 0x7ffd87bbb444
*y = 42
x = 43
&x = 0x7ffd87bbb444
y = 0x7ffd87bbb444
*y = 43

Key points:

  1. address of both x and y are stack
  2. y is pointing to the address of x
  3. updating the data pointed to by y causes x to be updated (because y is pointing to the address of x)
  4. there's no heap references here

Another concept that is related to this is "pass by reference" vs "pass by copy", so you may want to read about that.

EDIT: fixed a typo (higher/lower with respect to stack memory address). I'll blame my rambling on being unemployed.

[-] vrek@programming.dev 1 points 1 day ago

Wow, ok I guess I have a lot more to learn. I get the general concept of pointers/references and stack/heap. I kinda partially understand reading asm but didn't know about the rsp and rsb special memory locations, more familiar with like rax or eax but those are just general purpose registries, and that's mostly just from playing with https://godbolt.org/ if you're not familiar it's a website to convert code(many supported languages) to asm with several compilers and options similar to your g++ command but web based so I don't know how it would handle low level memory checking like this(also can't execute code, just display the asm code).

[-] Lee@retrolemmy.com 1 points 1 day ago* (last edited 1 day ago)

Wow, ok I guess I have a lot more to learn.

I think it depends on how low level you want to go. The benefit of using these kind of languages is that they abstract away the hardware so that you don't have to worry about the details. Granted details can matter when you're trying to push performance to the limits or be incredibly efficient, but that's so rare for people to do because it can be very time consuming and on modern CPUs, a compiler can often do optimizations that are better than a human would, especially when taking in to account advanced CPU features. With Out of Order Execution, branch prediction, pipeline flushes, and so on, trying to manually optimize stuff at a low level can result in things actually being less efficient (on a modern CPU) even though it would have been more efficient on a 1980s CPU (granted, my current hobby project is with a CPU popular in the 1980s -- Motorola 68K).

What I think is related to stack vs heap that's actually worth learning:

  1. new/malloc are slow
  2. pointers (don't worry about stack vs heap for this)
  3. variable scope/lifetime -- memory allocated via new/malloc stays allocated until it is deallocated with delete/free. Messing this up can cause memory leaks or other types of bugs and from what I understand, something that Rust is supposed to do better (but I've not looked at Rust, so don't really know for sure)

I think those are necessary. If you're wanting to go a little deeper on memory stuff, but not quite down to assembly, you could learn about memory pages/page tables/how operating systems handle memory allocations/virtual memory. Those topics are not C or C++, but more general computer/OS.

[-] vrek@programming.dev 1 points 1 day ago

Well my current project is not really performance critical within reason. Basically trying to write a program to handle a bunch of statistics for manufacturing. If you know what minitab is, kinda similar but multi-platform(gui is qt based) and removes some pain points. So for example if your company makes cuts wooden planks and you measure them, you can track those measurements and get a warning if something is going on before you make bad product(maybe something is wrong with machine, maybe it's high humidity causing wood to swell, maybe it's a new operator who needs training... Figuring that out is the engineers job)- . An example of additional feature is ability to generate a gage r&r plan sheet for a new measurement system. A gage r&r is often 10 samples, tested 3 times each by 3 different operators to see how repeatable and reproducible(hence r&r) your measurement system is. But you want the order randomized to eliminate hidden changes(for example humidity decreasing from morning to afternoon) how ever it's often easier to "borrow" an operator for an 2 hours then another then another than to borrow 3 operators for 6 hours. Minitab has the ability to generate a run order to do this but it randomizes both samples and operator meaning you need 3 operators for 6 hours. People got around this by faking their "randomness", one person I knew even tried to do this with a d10 die. This tends to not be real random and is easy to make a mistake(for example person with d10 didn't notice they rolled a 7 twice on the third run of the second operator). We had to investigate this failure, write a report of our findings and repeat the who test. I want to give an option to randomize samples and operators or only operators.

All the math for this stuff is pretty well known, proven and documented. The only performance I'm concerned with is with large datasets. For example we had one process that produced 5-7k data points per day, they had data going back till like 2010. The file was like 2 gig and took a good 5 minutes to open. I want to create a new file for each process, each year(time range may be adjustable). Then you have "product" file you open, then you select which processes you are interested in and the time frame you are interested in. All your data is then loaded(the product file effectively just has links to all associated process files for each year with data) and graphs all updated based only on the data you selected(my company reviewed data for previous month but had to keep records for up to 100 years for legal reasons). That should cut out most my performance concerns. But 7k data points on one process per day over 6 months is 1,281,000 data points so still have to be a little concerned.

I'm also unemployed so I guess that's why I'm ranting uncontrollably too. 😋

this post was submitted on 01 Sep 2026
17 points (100.0% liked)

Learn Programming

2231 readers
53 users here now

Posting Etiquette

  1. Ask the main part of your question in the title. This should be concise but informative.

  2. Provide everything up front. Don't make people fish for more details in the comments. Provide background information and examples.

  3. Be present for follow up questions. Don't ask for help and run away. Stick around to answer questions and provide more details.

  4. Ask about the problem you're trying to solve. Don't focus too much on debugging your exact solution, as you may be going down the wrong path. Include as much information as you can about what you ultimately are trying to achieve. See more on this here: https://xyproblem.info/

Icon base by Delapouite under CC BY 3.0 with modifications to add a gradient

founded 3 years ago
MODERATORS