我正在研究K&R C,这条线对我很突出:
指针是包含变量地址的变量。
我一直以为(也许是错误地)在引擎盖下的变量必须包含名称,类型和内存中某个位置的地址。即:虽然可以将变量视为值,但是编译器必须知道这些值在内存中的存储位置,因此变量也必须是指针(从概念上讲,不是形式上的)。
但是现在我不太确定。文字似乎暗示变量在某种程度上比指针更基本。
什么是变量,真的吗?他们是喜欢引擎盖下的指针,还是在某种程度上有所不同?特别希望在如何分配内存的上下文中了解这一点。
编辑:对于那些从事语义辩论的人……我有兴趣了解_average_用例,尽管我应该指定标准,但不是标准所指定的内容。出于功能目的,假设C是在unix机器上用gcc或类似语言编译的。谢谢!
答案 0 :(得分:3)
确切构成“变量”的内容因语言而异。使用哪种运行时环境也很重要-本机二进制(C / C ++ / Fortran / Cobol / Pascal),虚拟机中的字节码(Java / C#/ Scala / F#),源级别的解释器(old-skool) BASIC,bash / csh / sh)等
对于C来说,变量只是一块足以容纳指定类型值的内存块-没有与该内存块关联的元数据可以告诉您有关其名称的任何信息(通常不是IOW,如果您在运行的程序中检查了内存中的整数变量,那么您所看到的就是存储在该整数中的值。您不会看到有关该变量 的任何其他信息。
在翻译期间(即在编译代码时),编译器维护一个内部表,该表跟踪变量,变量名,类型,范围,可见性等。但是,没有该信息(通常)将其纳入生成的机器代码中。 auto
(局部)变量通常由距给定堆栈地址的偏移量引用。 static
变量通常具有固定地址。通过使用不同的机器代码指令来处理不同类型的值(例如,通常使用单独的指令来处理整数与浮点数)。
指针变量仅存储地址。该地址的确切格式将因系统而异,但是在现代x86和类似系统上,它实际上是一个无符号整数值。在分段存储系统上,它可能是一对值(页面编号和偏移量)。
编辑
C代码通常被编译为本机二进制文件(尽管至少有一个针对Java VM的编译器,并且可能有针对其他虚拟机的编译器)。在类似x86的系统上,正在运行的本机二进制文件通常这样布置在(虚拟!)内存中:
+-------------------------+
High address: | Environmental variables |
| and command line args |
+-------------------------+
| Stack |
| | |
| V |
| ^ |
| | |
| Heap |
+-------------------------+
| Read-only data items |
+-------------------------+
| Global data items |
+-------------------------+
| Program text (machine |
Low address: | code) |
+-------------------------+
具体细节因系统而异,但这是一个不错的整体视图。
每次调用一个函数(包括main
)时,都会从堆栈中获取内存以构建称为 stack frame 的堆栈。堆栈帧包含用于函数参数(如果有),局部变量(如果有),上一个堆栈帧的地址以及函数返回后要执行的下一条指令的地址的空间。
+--------------------+
High address: | Function arguments |
+--------------------+
| Return address |
+--------------------+
| Prev frame address | <-- %rbp/%ebp (frame pointer)
+--------------------+
Low address: | Local variables | <-- %rsp/%esp (stack pointer)
+--------------------+
%rsp
(64位)或%esp
(32位)寄存器存储堆栈顶部的地址(在x86上,堆栈朝着减少的地址“向下”增长), %rbp
(64位)或%ebp
(32位)寄存器存储堆栈帧的地址。通过框架指针的偏移量来引用函数参数和局部变量,例如
-4(%rpb) -- object starting 4 bytes "below" current frame address
32(%rbp) -- object starting 32 bytes "above" current frame address
这里是一个例子-我们有一个函数foo
,它带有两个int
参数和两个int
局部变量:
#include <stdio.h>
void foo( int x, int y )
{
int a;
int b;
a = 2 * x + y;
b = x - y;
printf( "x = %d, y = %d, a = %d, b = %d\n", x, y, a, b );
}
以下是该功能(MacOS 10.13,LLVM版本9.1.0)生成的程序集:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _foo ## -- Begin function foo
.p2align 4, 0x90
_foo: ## @foo
.cfi_startproc
## BB#0:
pushl %ebp
Lcfi0:
.cfi_def_cfa_offset 8
Lcfi1:
.cfi_offset %ebp, -8
movl %esp, %ebp
Lcfi2:
.cfi_def_cfa_register %ebp
pushl %ebx
pushl %edi
pushl %esi
subl $60, %esp
Lcfi3:
.cfi_offset %esi, -20
Lcfi4:
.cfi_offset %edi, -16
Lcfi5:
.cfi_offset %ebx, -12
calll L0$pb
L0$pb:
popl %eax
movl 12(%ebp), %ecx
movl 8(%ebp), %edx
leal L_.str-L0$pb(%eax), %eax
movl 8(%ebp), %esi
shll $1, %esi
addl 12(%ebp), %esi
movl %esi, -16(%ebp)
movl 8(%ebp), %esi
subl 12(%ebp), %esi
movl %esi, -20(%ebp)
movl 8(%ebp), %esi
movl 12(%ebp), %edi
movl -16(%ebp), %ebx
movl %eax, -24(%ebp) ## 4-byte Spill
movl -20(%ebp), %eax
movl %eax, -28(%ebp) ## 4-byte Spill
movl -24(%ebp), %eax ## 4-byte Reload
movl %eax, (%esp)
movl %esi, 4(%esp)
movl %edi, 8(%esp)
movl %ebx, 12(%esp)
movl -28(%ebp), %esi ## 4-byte Reload
movl %esi, 16(%esp)
movl %edx, -32(%ebp) ## 4-byte Spill
movl %ecx, -36(%ebp) ## 4-byte Spill
calll _printf
movl %eax, -40(%ebp) ## 4-byte Spill
addl $60, %esp
popl %esi
popl %edi
popl %ebx
popl %ebp
retl
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "x = %d, y = %d, a = %d, b = %d\n"
.subsections_via_symbols
这是我们的堆栈框架的样子:
+---+
High address: | y |
+---+
| x |
+---+
| | return address
+---+
| | address of previous frame
+---+
| a |
+---+
| b |
+---+
现在,这就是32位世界中的情况。 64位变得有点复杂-一些函数参数是在寄存器中而不是在堆栈中传递的,因此上面的漂亮图片崩溃了。
现在,我正在谈论运行时变量 的概念,这就是我想问的问题。
答案 1 :(得分:0)
我一直以为(可能是错误地)在幕后的变量必须包含名称,类型和内存中某个位置的地址。
至少在C11中,这是错误的。最终参考是标准规范,例如n1570(实际上是与ISO标准相同的最新草案)
实际上,变量通常是一些 memory location 。它有一些值,但是名称和类型在运行时被忘记了。只有编译器知道变量的名称和类型。如果编译器正在优化,有时(在as-if rule下)可能会忘记该变量。
指针不是指向变量,而是指向内存位置。
也请阅读有关undefined behavior的信息。
答案 2 :(得分:0)
@IBDesignable
public class LineHighlightedLabel: UILabel {
public override func draw(_ rect: CGRect) {
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage.init(attributedString: attributedText!)
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer.init(size: bounds.size)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
layoutManager.addTextContainer(textContainer)
layoutManager.enumerateLineFragments(forGlyphRange: NSMakeRange(0, textStorage.length)) { (rect, usedRect, textContainer, glyphRange, bool) in
let lineRect = CGRect(x: usedRect.origin.x, y: usedRect.origin.y + 1, width: usedRect.size.width, height: usedRect.size.height - 2)
UIColor.green.setStroke()
let lineRectPath = UIBezierPath(roundedRect: lineRect, cornerRadius: 5)
lineRectPath.lineWidth = 0.5
lineRectPath.stroke()
}
super.drawText(in: rect)
}
public override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
}
是C的抽象事物,仅在源代码中具有名称(它也放置在目标文件中(如果编译器生成了它们)-但不在此讨论范围之内)。在已编译(并可能链接到)的可执行文件中,没有变量-内存或寄存器中只有一些位置,这些位置由机器代码指令操纵。
变量是一种语言抽象,它们不在源代码范围之内。 SIn源代码变量具有名称和类型。据我们所知,在可执行文件中,C语言中的变量不存在。
答案 3 :(得分:-1)
变量是内存中特定位置的符号表示。
该位置包含与变量关联的类型和值。存储在该内存位置中的数据可以更改,因此称为变量。
如上所述,指针是一个变量,其中类型是指针,值是内存中的地址。
答案 4 :(得分:-1)
变量表示一个对象,该对象包含一些数据,并且具有某种类型。数据始终以二进制形式物理存储在内存中。对象的类型决定如何解释数据。
所以指针就是这样一个对象,它包含的数据是内存中的地址,类型是“指向另一个对象的指针”。
变量引用的对象也是实体对象,因此它具有自己的大小和位置(内存地址)之类的属性。这不会干扰其包含的内容(其“数据”是什么)。
考虑一个生动的例子。路标可能指向另一个位置,但路标本身位于某处。这两个位置互不干扰,甚至可以相同(尽管通常没有人这样做)。
答案 5 :(得分:-1)
从wikipedia article,(强调我的)
在计算机编程中,变量或标量是存储位置(由内存地址标识)与相关的符号名(标识符)配对,其中包含一些已知或未知数量的引用信息作为价值。 变量名称是引用存储值的常用方式,除了引用变量本身之外,还取决于上下文。名称和内容的这种分隔允许名称的使用独立于其代表的确切信息。 计算机源代码中的标识符可以在运行时绑定到一个值,因此变量的值可以在程序执行过程中更改。 [....]
然后,继续前进
...所以变量也必须是指针...
嗯,不是真的。仅仅因为变量与地址关联并不能使它们成为指针类型。
要添加的指针类型也可以是变量-基本上是一个变量,用于保存另一种类型(变量或常量对象)的地址。
以图形方式呈现,让我们考虑两个变量
int a
-类型为a
的名为int
的变量(假定整数大小为4个字节)int *ptr
-类型为ptr
的名为int *
的变量(假设指针大小为4个字节)我们说ptr = &a;
如上所述:
+--------------------+
| |
| |
+--------------------+
8000 a 8003
// Here you have four bytes, ranging from 8000 to 8003, indicated by variable `a`
+----------------------+
| |
| |
+----------------------+
9000 ptr 9003
// Here you have four bytes, ranging from 9000 to 9003, indicated by variable `ptr`
// next, as we say ptr = &a;
+----------------------+
| |
| 8000 |
+----------------------+
9000 ptr 9003
//ptr now holds the address of variable `a`.
// however, ptr still has it's own address, as it itself, is a variable.
答案 6 :(得分:-1)
假设您具有以下变量:
@Temp_Table
编译器丢弃名称int x = 1234;
void f(void) {
int y = 4567;
}
和x
和类型y
,只记住变量的地址(它可以将名称保留在符号映射中进行调试,但是这些是否则不需要)。
诸如int
之类的静态变量具有固定的地址,因此,当您的代码对x
执行某些操作时,您将告诉编译器使用此固定地址处的值进行处理。
诸如x
之类的自动变量通常保存在寄存器中。编译器生成代码以查看该寄存器的值。否则编译器可能会将其存储在y
的堆栈帧中,因此它将生成代码,该代码查看与该堆栈帧地址偏移的地址的内容。
作为指针的变量以相同的方式工作,除了代替存储f()
之类的值之外,它存储另一个变量的地址。