怎样制作一个个人网站,个人网站开发的背景,深圳建设网站费用明细,vps做网站空间指针、数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer afithmetic)和C内部处理数组的方式。首先#xff0c;我
们来看一看算术。将整数变量加1后#xff0c;其值将增加1#xff1b;但将指针变量加1后#xff0c;增加的量等于它指向的类型
的字节数。将指向d…指针、数组和指针算术指针和数组基本等价的原因在于指针算术(pointer afithmetic)和C内部处理数组的方式。首先我们来看一看算术。将整数变量加1后其值将增加1但将指针变量加1后增加的量等于它指向的类型的字节数。将指向double的指针加1后如果系统对double使用8个字节存储则数值将增加8将指向short的指针加1后如果系统对short使用2个字节存储则指针值将增加2。程序清单4.19演示了这种令人吃惊的现象它还说明了另一点C将数组名解释为地址。// 指针、数组和指针算术.cpp : 此文件包含 main 函数。程序执行将在此处开始并结束。 // #include iostream int main() { using namespace std; double wages[3] { 10000.0,20000.0,30000.0 }; short stacks[3] { 3,2,1 }; double* pw wages; short* ps stacks[0]; cout pw pw ,*pw *pw endl; pw pw 1; cout pw pw ,*pw *pw \n\n; cout ps ps ,*ps *ps endl; ps ps 1; cout add 1 to the ps pointer:\n; cout ps ps ,*ps *ps \n\n; cout access two elements with array notation\n; cout stacks[0] stacks[0] ,stacks[1] stacks[1] endl; cout access two elements with pointer notation\n; cout *stacks *stacks ,*(stacks1) *(stacks 1) endl; cout sizeof(wages) size of wages array\n; cout sizeof(pw) size of pw pointer\n; return 0; }运行结果pw 00000001000FF738,*pw 10000 pw 00000001000FF740,*pw20000 ps00000001000FF764,*ps3 add 1 to the ps pointer: ps 00000001000FF766,*ps2 access two elements with array notation stacks[0] 3,stacks[1] 2 access two elements with pointer notation *stacks 3,*(stacks1)2 24 size of wages array 8 size of pw pointer程序说明在多数情况下C将数组名解释为数组第1个元素的地址。因此下面的语句将pw声明为指向double类型的指针然后将它初始化为wages—wages数组中第1个元素的地址double *pwwages;和所有数组一样wages 也存在下面的等式wageswages[0]address of first element of array为表明情况确实如此该程序在表达式stacks[0]中显式地使用地址运算符来将ps 指针初始化为stacks数组的第1 个元素。接下来程序查看pw 和pw 的值。前者是地址后者是存储在该地址中的值。由于pw 指向第1 个元素因此pw 显示的值为第1 个元素的值即10000。接着程序将pw 加1。正如前面指出的这样数字地址值将增加8这使得pw 的值为第2 个元素的地址。因此*pw 现在的值是20000—第2 个元素的值参见图4.10为使改图更为清晰对其中的地址值做了调整。此后程序对ps执行相同的操作。这一次由于ps指向的是short类型而short占用2个字节因此将指针加1时其值将增加2。结果是指针也指向数组中下一个元素。注意将指针变量加1后其增加的值等于指向的类型占用的字节数。现在来看一看数组表达式stacks[1]。C编译器将该表达式看作是*stacks1)这意味着先计算数组第2个元素的地址然后找到存储在那里的值。最后的结果便是stacks[1]的含义运算符优先级要求使用括号如果不使用括号将给*stacks加1,而不是给stacks加1)。从该程序的输出可知(stacks1)和stacks[1]是等价的。同样stacks2和stacks[2]也是等价的。通常使用数组表示法时C都执行下面的转换arrayname[i] becomes *(arrayname i)如果使用的是指针而不是数组名则C也将执行同样的转换pointername[i] becomes *(pointername i)因此在很多情况下可以相同的方式使用指针名和数组名。对于它们可以使用数组方括号表示法也可以使用解除引用运算符*。在多数表达式中它们都表示地址。区别之一是可以修改指针的值而数组名是常量pointernamepointername1;//valid arraynamearrayname1; //not allowed另一个区别是对数组应用sizeof 运算符得到的是数组的长度而对指针应用sizeof 得到的是指针的长度即使指针指向的是一个数组。例如在程序清单4.19 中pw 和wages 指的是同一个数组但对它们应用sizeof 运算符得到的结果如下24size of wages arraydisplaying sizeof wages 4size of pw pointerdisplaying sizeof pw总之使用new 来创建数组以及使用指针来访问不同的元素很简单。只要把指针当作数组名对待即可。然而要理解为何可以这样做将是一种挑战。要想真正了解数组和指针应认真复习它们的相互关系。指针小结刚才已经介绍了大量指针的知识下面对指针和数组做一总结。1声明指针要声明指向特定类型的指针请使用下面的格式typeName * pointerName;下面是一些示例double *pn; char *pc;其中pn 和pc 都是指针而double *和char *是指向double 的指针和指向char 的指针。2给指针赋值应将内存地址赋给指针。可以对变量名应用运算符来获得被命名的内存的地址new运算符返回未命名的内存的地址。下而是一些示例double *pn; //pn can point to a double value double *pa; //so can pa char *pc; //pc can point to a char value double buddle3.2; pnbubble; //assign address of bubble to pn pcnew char; //assign address of newly allocated char memory to pc panew double[30]; //assign address of lst element of array of 30 double to pa3对指针解除引用对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符来解除引用。因此如果像上面的例子中那样pn 是指向bubble 的指针则pn 是指向的值即3.2。下面是一些示例cout*pn; //print the value of bubble *pc S; //place S into the memory location whose address is pc另一种对指针解除引用的方法是使用数组表示法例如pn[0]与*pn是一样的。决不要对未被初始化为适当地址的指针解除引用。4区分指针和指针所指向的值如果pt是指向int的指针则*pt不是指向int的指针而是完全等同于一个int类型的变量。pt才是指针。下面是一些示例int *ptnew int; //assigns an address to thhe pointer pt *pt 5; //stores the value 5 at that address5数组名在多数情况下C将数组名视为数组的第一个元素的地址。下面是一个示例int tacos[10]; //now tacos is the same as tacos[0]一种例外情况是将sizeof 运算符用于数组名用时此时将返回整个数组的长度单位为字节。6指针算术C允许将指针和整数相加。加1 的结果等于原来的地址值加上指向的对象占用的总字节数。还可以将一个指针减去另一个指针获得两个指针的差。后一种运算将得到一个整数仅当两个指针指向同一个数组也可以指向超出结尾的一个位置时这种运算才有意义这将得到两个元素的间隔。下面是一些示例int tacos[10]{5,2,8,4,1,2,2,4,6,8}; int *pttacos; //suppose pf and tacos are the address 3000 ptpt1; //now pt is 3004 if a int is 4 bytes int *petacos[9]; //pe is 3036 if an int is 4 bytes pepe-1; //now pe is 3032,the address of tacos[8] int diffpe - pt; //diff is 7,the separation between tacos[8] and tacos[1]7数组的动态联编和静态联编使用数组声明来创建数组时将采用静态联编即数组的长度在编译时设置int tacos[10]; //static binding,size fixed at compile time使用new[ ]运算符创建数组时将采用动态联编动态数组即将在运行时为数组分配空间其长度也将在运行时设置。使用完这种数组后应使用delete [ ]释放其占用的内存int size; cinsize; int *ptnew int[size]; //dynamic binding,size set at run time ... delete[] pz; //free memory when finished8 数组表示法和指针表示法使用方括号数组表示法等同于对指针解除引用tacos[0] means *tacos means the value at address tacos tacos[3] means *(tacos 3) means the value at address tacos 3数组名和指针变量都是如此因此对于指针和数组名既可以使用指针表示法也可以使用数组表示法。下面是一些示例int *ptnew int[10]; //pt points to block of 10 ints *pt5; //set element number 0 to 5 pt[0]6; //reset element number 0 to 6 pt[9]44; //set tenth element(element number 9) to 44 int coats[10]; *(coats4)12; //set coats[4] to 12指针和字符串数组和指针的特殊关系可以扩展到C-风格字符串。请看下面的代码char flower[10]rose; coutflowers are red\n;数组名是第一个元素的地址因此cout语句中的flower是包含字符r的char元素的地址。cout对象认为char的地址是字符串的地址因此它打印该地址处的字符然后继续打印后面的字符直到遇到空字符\0为止。总之如果给cout提供一个字符的地址则它将从该字符开始打印直到遇到空字符为止。这里的关键不在于flower是数组名而在于flower是一个char的地址。这意味着可以将指向char的指针变量作为cout的参数因为它也是char的地址。当然该指针指向字符串的开头稍后将核实这一点。前面的cout语句中最后一部分的情况如何呢如果flower是字符串第一个字符的地址则表达式“s are red\n”是什么呢为了与cout对字符串输出的处理保持一致这个用引号括起的字符串也应当是一个地址。在C中用引号括起的字符串像数组名一样也是第一个元素的地址。上述代码不会将整个字符串发送给cout而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串处理的方式是一样的都将传递它们的地址。与逐个传递字符串中的所有字符相比这样做的工作量确实要少。注意在cout 和多数C表达式中char 数组名、char 指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。程序清单4.20 演示了如何使用不同形式的字符串。它使用了两个字符串库中的函数。函数strlen( )我们以前用过它返回字符串的长度。函数strcpy( )将字符串从一个位置复制到另一个位置。这两个函数的原型都位于头文件cstring在不太新的实现中为string.h中。该程序还通过注释指出了应尽量避免的错误使用指针的方式。#define _CRT_SECURE_NO_WARNINGS #include iostream #include cstring int main() { using namespace std; char animal[20] bear; // animal holds bear const char* bird wren; // bird holds address of string char* ps; // uninitialized cout animal and; // display bear cout bird \n; // display wren cout Enter a kind of animal: ; cin animal; ps animal; cout ps !\n; cout Before using strcpy():\n; cout animal at (int*)animal endl; cout ps at (int*)ps endl; ps new char[strlen(animal) 1]; // get new storage strcpy(ps, animal); // copy string to new storage cout After using strcpy():\n; cout animal at (int*)animal endl; cout ps at (int*)ps endl; delete[] ps; return 0; }运行结果bear andwren Enter a kind of animal: fox fox! Before using strcpy(): fox at 0000000D382FF9C8 fox at 0000000D382FF9C8 After using strcpy(): fox at 0000000D382FF9C8 fox at 000001F897C3E020程序说明程序清单4.20中的程序创建了一个char数组(animal)和两个指向char的指针变量(bird和ps)。该程序首先将animal数组初始化为字符串bear”就像初始化数组一样。然后程序执行了一些新的操作将char指针初始化为指向一个字符串const char *birdwren; //bird holds address of string记住wren“实际表示的是字符串的地址因此这条语句将wren”的地址赋给了指针。一般来说编译器在内存留出一些空间以存储程序源代码中所有用引号括起的字符串并将每个被存储的字符串与其地址关联起来。这意味着可以像使用字符串wren”那样使用指针bird如下面的示例所示cout A concerned bird speaks\n;字符串字面值是常量这就是为什么代码在声明中使用关键字const 的原因。以这种方式使用const 意味着可以用bird 来访问字符串但不能修改它。第7 章将详细介绍const 指针。最后指针ps 未被初始化因此不指向任何字符串正如您知道的这通常是个坏主意这里也不例外。接下来程序说明了这样一点即对于cout 来说使用数组名animal 和指针bird 是一样的。毕竟它们都是字符串的地址cout 将显示存储在这两个地址上的两个字符串“bear”和“wren”。如果激活错误地显示ps 的代码则将可能显示一个空行、一堆乱码或者程序将崩溃。创建未初始化的指针有点像签发空头支票无法控制它将被如何使用。对于输入情况有点不同。只要输入比较短能够被存储在数组中则使用数组animal 进行输入将是安全的。然而使用bird 来进行输入并不合适有些编译器将字符串字面值视为只读常量如果试图修改它们将导致运行阶段错误。在C中字符串字面值都将被视为常量但并不是所有的编译器都对以前的行为做了这样的修改。有些编译器只使用字符串字面值的一个副本来表示程序中所有的该字面值。下面讨论一下第二点。C不能保证字符串字面值被唯一地存储。也就是说如果在程序中多次使用了字符串字面值“wren”则编译器将可能存储该字符串的多个副本也可能只存储一个副本。如果是后面一种情况则将bird 设置为指向一个“wren”将使它只是指向该字符串的唯一一个副本。将值读入一个字符串可能会影响被认为是独立的、位于其他地方的字符串。无论如何由于bird 指针被声明为const因此编译器将禁止改变bird 指向的位置中的内容。试图将信息读入ps 指向的位置将更糟。由于ps 没有被初始化因此并不知道信息将被存储在哪里这甚至可能改写内存中的信息。幸运的是要避免这种问题很容易—只要使用足够大的char 数组来接收输入即可。请不要使用字符串常量或未被初始化的指针来接收输入。为避免这些问题也可以使用std::string 对象而不是数组。警告在将字符串读入程序时应使用已分配的内存地址。该地址可以是数组名也可以是使用new初始化过的指针。接下来请注意下述代码完成的工作psanimal; ... coutanimal at(int *)animalendl; coutps at(int *)psendl;一般来说如果给cout 提供一个指针它将打印地址。但如果指针的类型为char *则cout 将显示指向的字符串。如果要显示的是字符串的地址则必须将这种指针强制转换为另一种指针类型如int *上面的代码就是这样做的。因此ps 显示为字符串“fox”而int *ps 显示为该字符串的地址。注意将anim 赋给ps 并不会复制字符串而只是复制地址。这样这两个指针将指向相同的内存单元和字符串。要获得字符串的副本还需要做其他工作。首先需要分配内存来存储该字符串这可以通过声明另一个数组或使用new来完成。后一种方法使得能够根据字符串的长度来指定所需的空间psnew char[strlen(animal)1];字符串“fox”不能填满整个animal 数组因此这样做浪费了空间。上述代码使用strlen( )来确定字符串的长度并将它加1 来获得包含空字符时该字符串的长度。随后程序使用new 来分配刚好足够存储该字符串的空间。接下来需要将animal 数组中的字符串复制到新分配的空间中。将animal 赋给ps 是不可行的因为这样只能修改存储在ps 中的地址从而失去程序访问新分配内存的唯一途径。需要使用库函数strcpy( )strcpy(ps,animal);strcpy( )函数接受2 个参数。第一个是目标地址第二个是要复制的字符串的地址。您应确定分配了目标空间并有足够的空间来存储副本。在这里我们用strlen( )来确定所需的空间并使用new 获得可用的内存。通过使用strcpy( )和new将获得“fox”的两个独立副本另外new 在离animal 数组很远的地方找到了所需的内存空间。经常需要将字符串放到数组中。初始化数组时请使用运算符否则应使用strcpy( )或strncpy( )。strcpy( )在前面已经介绍过其工作原理如下char food[20]carrots; //initialization strcpy(food,flan); //otherwise注意类似下面这样的代码可能导致问题因为food数组比字符串小strcpy(food,a picnic basket filled with many goodies);在这种情况下函数将字符串中剩余的部分复制到数组后面的内存字节中这可能会覆盖程序正在使用的其他内存。要避免这种问题请使用strncpy( )。该函数还接受第3 个参数—要复制的最大字符数。然而要注意的是如果该函数在到达字符串结尾之前目标内存已经用完则它将不会添加空字符。因此应该这样使用该函数strncpy(food,a picnic basket filled with many goodies,19); food[19]\0;这样最多将19 个字符复制到数组中然后将最后一个元素设置成空字符。如果该字符串少于19 个字符则strncpy( )将在复制完该字符串之后加上空字符以标记该字符串的结尾。警告应使用strcpy( )或strncpy( )而不是赋值运算符来将字符串赋给数组。您对使用C-风格字符串和cstring 库的一些方面有了了解后便可以理解为何使用C string 类型更为简单了您不用担心字符串会导致数组越界并可以使用赋值运算符而不是函数strcpy( )和strncpy( )。使用new 创建动态结构在运行时创建数组优于在编译时创建数组对于结构也是如此。需要在程序运行时为结构分配所需的空间这也可以使用new运算符来完成。通过使用new可以创建动态结构。同样“动态”意味着内存是在运行时而不是编译时分配的。由于类与结构非常相似因此本节介绍的有关结构的技术也适用于类。将new用于结构由两步组成创建结构和访问其成员。要创建结构需要同时使用结构类型和new。例如要创建一个未命名的inflatable类型并将其地址赋给一个指针可以这样做inflatable *psnew inflatable;这将把足以存储inflatable 结构的一块可用内存的地址赋给ps。这种句法和C的内置类型完全相同。比较棘手的一步是访问成员。创建动态结构时不能将成员运算符句点用于结构名因为这种结构没有名称只是知道它的地址。C专门为这种情况提供了一个运算符箭头成员运算符−。该运算符由连字符和大于号组成可用于指向结构的指针就像点运算符可用于结构名一样。例如如果ps 指向一个inflatable 结构则ps−price 是被指向的结构的price 成员参见图4.11。提示有时C新手在指定结构成员时搞不清楚何时应使用句点运算符何时应使用箭头运算符。规则非常简单。如果结构标识符是结构名则使用句点运算符如果标识符是指向结构的指针则使用箭头运算符。另一种访问结构成员的方法是如果ps 是指向结构的指针则ps 就是被指向的值—结构本身。由于ps 是一个结构因此*ps.price 是该结构的price 成员。C的运算符优先规则要求使用括号。程序清单4.21 使用new 创建一个未命名的结构并演示了两种访问结构成员的指针表示法。#include iostream struct inflatable //structure definition { char name[20]; float volume; double price; }; int main() { using namespace std; inflatable* ps new inflatable; cout Enter name of inflatable item:; cin.get(ps-name, 20); cout Enter volume in cubic feet:; cin (*ps).volume; cout Enter price:$; cin ps-price; cout Name: (*ps).name endl; cout Volume: ps-volume cubic feet\n; cout Price:$ ps-price endl; delete ps; return 0; }运行结果Enter name of inflatable item:Fabulous Frodo Enter volume in cubic feet:1.4 Enter price:$27.99 Name:Fabulous Frodo Volume:1.4 cubic feet Price:$27.991一个使用new 和delete 的示例下面介绍一个使用new 和delete 来存储通过键盘输入的字符串的示例。程序清单4.22 定义了一个函数getname( )该函数返回一个指向输入字符串的指针。该函数将输入读入到一个大型的临时数组中然后使用new [ ]创建一个刚好能够存储该输入字符串的内存块并返回一个指向该内存块的指针。对于读取大量字符串的程序这种方法可以节省大量内存实际编写程序时使用string 类将更容易因为这样可以使用内置的new 和delete。假设程序要读取100 个字符串其中最大的字符串包含79 个字符而大多数字符串都短得多。如果用char 数组来存储这些字符串则需要1000 个数组其中每个数组的长度为80 个字符。这总共需要80000个字节而其中的很多内存没有被使用。另一种方法是创建一个数组它包含1000个指向char 的指针然后使用new根据每个字符串的需要分配相应数量的内存。这将节省几万个字节。是根据输入来分配内存而不是为每个字符串使用一个大型数组。另外还可以使用new根据需要的指针数量来分配空间。就目前而言这有点不切实际即使是使用1000个指针的数组也是这样不过程序清单4.22还是演示了一些技巧。另外为演示delete是如何工作的该程序还用它来释放内存以便能够重新使用。#define _CRT_SECURE_NO_WARNINGS #include iostream #include string using namespace std; char* getname(void); int main() { char* name; name getname(); cout name at (int*)name \n; delete[] name; name getname(); cout name at (int*)name \n; delete[] name; return 0; } char* getname() { char temp[80]; cout Enter last name:; cin temp; char* pn new char[strlen(temp) 1]; strcpy(pn, temp); return pn; }运行结果Enter last name:Fredeldumpkin Fredeldumpkin at0000024F3C6AEE80 Enter last name:Pook Pook at0000024F3C6AE5C02程序说明来看一下程序清单4.22 中的函数getname( )。它使用cin 将输入的单词放到temp 数组中然后使用new分配新内存以存储该单词。程序需要strletemp 1 个字符包括空字符来存储该字符串因此将这个值提供给new。获得空间后getname( )使用标准库函数strcpy( )将temp 中的字符串复制到新的内存块中。该函数并不检查内存块是否能够容纳字符串但getname( )通过使用new 请求合适的字节数来完成了这样的工作。最后函数返回pn这是字符串副本的地址。在main( )中返回值地址被赋给指针name。该指针是在main( )中定义的但它指向getname( )函数中分配的内存块。然后程序打印该字符串及其地址。接下来在释放name 指向的内存块后main( )再次调用getname( )。C不保证新释放的内存就是下一次使用new 时选择的内存从程序运行结果可知确实不是。在这个例子中getname( )分配内存而main( )释放内存。将new 和delete 放在不同的函数中通常并不是个好办法因为这样很容易忘记使用delete。不过这个例子确实把new 和delete 分开放置了只是为了说明这样做也是可以的。为了解该程序的一些更为微妙的方面需要知道一些有关c是如何处理内存的知识。下面介绍一些这样的知识这些知识将在第9章做全面介绍。自动存储、静态存储和动态存储根据用于分配内存的方法C有3 种管理数据内存的方式自动存储、静态存储和动态存储有时也叫作自由存储空间或堆。在存在时间的长短方面以这3 种方式分配的数据对象各不相同。下面简要地介绍每种类型C11 新增了第四种类型—线程存储这将在第9 章简要地讨论。1自动存储在函数内部定义的常规变量使用自动存储空间被称为自动变量automatic variable这意味着它们在所属的函数被调用时自动产生在该函数结束时消亡。例如程序清单4.22 中的temp 数组仅当getname( )函数活动时存在。当程序控制权回到main( )时temp 使用的内存将自动被释放。如果getname( )返回temp的地址则main( )中的name 指针指向的内存将很快得到重新使用。这就是在getname( )中使用new 的原因之一。实际上自动变量是一个局部变量其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。到目前为止我们使用的所有代码块都是整个函数。然而在下一章将会看到函数内也可以有代码块。如果在其中的某个代码块定义了一个变量则该变量仅在程序执行该代码块中的代码时存在。自动变量通常存储在栈中。这意味着执行代码块时其中的变量将依次加入到栈中而在离开代码块时将按相反的顺序释放这些变量这被称为后进先出LIFO。因此在程序执行过程中栈将不断地增大和缩小。2静态存储静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种一种是在函数外面定义它另一种是在声明变量时使用关键字staticstatic double fee56.50;在KRC中只能初始化静态数组和静态结构而CRelease2.0及后续版本和ANSIC中也可以初始化自动数组和自动结构。然而一些您可能己经发现有些C实现还不支持对自动数组和自动结构的初始化。第9 章将详细介绍静态存储。自动存储和静态存储的关键在于这些方法严格地限制了变量的寿命。变量可能存在于程序的整个生命周期静态变量也可能只是在特定函数被执行时存在自动变量。动态存储new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池这在C中被称为自由存储空间store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。程序清单4.22表明new和delete让您能够在一个函数中分配内存而在另一个函数中释放它。因此数据的生命周期不完全受程序或函数的生存时间控制。与使用常规变量相比使用new和delete让程序员对程序如何使用内存有更大的控制权。然而内存管理也更复杂了。在栈中自动添加和删除机制使得占用的内存总是连续的但和delete的相互影响可能导致占用的自由存储区不连续这使得跟踪新分配内存的位置更困难。