-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChapter10.txt
More file actions
759 lines (562 loc) · 47.9 KB
/
Chapter10.txt
File metadata and controls
759 lines (562 loc) · 47.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
Chapter 10 Arrays and Pointers
一、Arrays
1.Initialization
(1)Array == composed of a series of elements of one data type.
[1]Array declaration == tells the compiler HOW MANY ELEMENTS the array contains and WHAT THE TYPE is for these elements.
[2]The brackets ([]) identify the arrays, and the number enclosed in the brackets indicates the number of elements in the array.
[3]To access elements in an array, you identify an individual element by using its subscript number (its index), which starts with 0.
[4]Single-valued variable == scalar variable.
[5]We initialize an array by using a comma-separated list of values enclosed in braces. We can use spaces between the values and the commas, if we want.
(2)Take a look at the day_mon1.c program:
[1]It is wrong only one month in every 4 years.
[2]This program used the symbolic constant MONTHS to represent the array size, which is common and recommended. If the world switched to a 13-month calendar, we just have to modify the #define statement and do not have to track down every place in the program that uses the array size.
[3]Read-only array == the program will retrieve values from the array, but it will not try to write new values into the array.
{1}We can use the const keyword when we declare and initialize the array.
{2}This way, it makes the program treat each element in the array as a constant.
(3)Take a look at the no_data.c program:
[1]The array members are like ordinary variables: if you do not initialize them, they might have any value. The compiler is allowed to just use whatever values were already present at those memory locations.
[2]The number of items in the list should match the size of the array.
(4)Take a look at the somedata.c program:
[1]When it ran out of values from the list, it initialized the remaining elements to 0. If you do not initialize an array at all, its elements, like uninitialized ordinary variables, get garbage values, but if you partially initialize an array, the remaining elements are set to 0.
[2]We can let the compiler match the array size to the list by omitting the size from the braces.
(5)Take a look at the day_mon2.c program:
[1]When you use empty brackets to initialize an array, the compiler counts the number of items in the list and makes the array that large. That is automatic counting.
[2]The sizeof operator gives the size, in bytes, of the object, or type, following it. Dividing the size of the entire array by the size of one element tells us how many elements are in the array.
[3]A potential disadvantage of automatic counting is that errors in the number of elements could pass unnoticed.
2.Designated Initializers (C99)
(1)C99 added a new capability: designated initializers == allows you to pick and choose which elements are initialized.
(2)With C99, we can use an index in brackets in the initialization list to specify a particular element.
For example:
-------------------------------------------------------------------------------------------------------------------
int arr[6] = {[5] = 212}; // initialize arr[5] to 212
-------------------------------------------------------------------------------------------------------------------
(3)As with regular initialization, after you initialize at least one element, the uninitialized elements are set to 0.
(4)Take a look at the designate.c program:
[1]If the code follows a designated initializer with further values, as in the sequence [4] = 31, 30, 31, these further values are used to initialize the subsequent elements.
[2]If the code initializes a particular element to a value more than once, the last initialization is the one that takes effect.
For example:
In the designate.c program, the start of the initialization list initializes days[1] to 28, but that is overridden by the [1] = 29 designated initialization later.
[3]?What if we do not specify the array size?
For example:
-------------------------------------------------------------------------------------------------------------------
int stuff[] = {1, [6] = 23}; // what happens?
int staff[] = {1, [6] = 4, 9, 10}; // what happens?
-------------------------------------------------------------------------------------------------------------------
The compiler will make the array big enough to accomodate the initialization values. So stuff will have 7 elements, numbered 0-6, and staff will have 2 more elements, or 9.
3.Assigning Array Values
(1)After an array has been declared, you can assign values to array members by using an array index, or subscript.
(2)C does not let you assign 1 array to another as a unit. Nor can you use the list-in-braces form except when initializing.
For example:
-------------------------------------------------------------------------------------------------------------------
/* nonvalid array assignment */
#define SIZE 5
int main(void)
{
int oxen[SIZE] = {5, 3, 2, 8}; /* ok here */
int yaks[SIZE];
yaks = oxen; /* not allowed */
yaks[SIZE] = oxen[SIZE]; /* out of range */
yaks[SIZE] = {5, 3, 2, 8}; /* does not work */
}
-------------------------------------------------------------------------------------------------------------------
4.Array Bounds
(1)We have to make sure we use array indices that are within bounds. That is, we have to make sure they have values valid for the array.
(2)Take a look at the bounds.c program:
[1]The compiler does not check to see whether the indices are valid. The result of using a bad index is, in the language of the C standard, undefined.
[2]When we run the program, it might seem to work, it might work oddly, or it might abort.
(3)Not checking bounds allows a C program to run faster.
(4)To be totally safe, the compiler would have to add extra code to check the value of each index during runtime, and that would slow things down.
5.Specifying an Array Size
(1)Until the C99 standard, the answer has been that you have to use a constant integer expression between the brackets when declaring an array.
(2)Constant integer expression == one formed from integer constants.
For example: a sizeof expression is considered an integer constant, but a const value IS NOT. And the value of the expression must be greater than 0.
These cases cannot be a constant integer expression:
-------------------------------------------------------------------------------------------------------------------
float a4[-4]; // no, size must be > 0
float a6[2.5]; // no, size must be an integer
float a8[n]; // not allowed before C99
-------------------------------------------------------------------------------------------------------------------
The last declaration create a new breed of array called variable-length array, or VLA for short.
(3)VLA == allow C to become a better language for numerical computing and make it easier to convert existing libraries of FORTRAN numerical calculation routines to C. It has some restrictions: for example, we cannot initialize a VLA in its declaration.
二、Multidimensional Arrays
1.Initializing a Two-Dimensional Array
(1)We can declare array of arrays like this:
-------------------------------------------------------------------------------------------------------------------
float rain[5][12]; // array of 5 arrays of 12 floats
-------------------------------------------------------------------------------------------------------------------
[1]One way to look at this declaration is to first look at the inner portion: rain[5] means rain is an array with 5 elements. And then look at the remaining part of the declaration which is [12], that means each element is of type float[12], which means that each of the 5 elements of rain is, in itself, an array of 12 float values. That means rain[0] is the 1st element of rain which is an array of 12 floats, and rain[0][0] is a float.
(2)Take a look at the rain.c program:
[1]The computation scheme
{1}A nested loop structure like this one is natual for handling a 2-dimensional array. One loop handles the 1st subscript, and the other loop handles the 2nd subscript.
{2}Each time the outer loop cycles once, the inner loop cycles its full allotment.
[2]The initialization
{1}Its initialization uses 5 embraced lists of numbers, all enclosed by 1 outer set of braces. The data in the 1st interior set of braces is assigned to the 1st row of the array, the data in the 2nd interior set goes to the 2nd row, and so on.
{2}Mismatches between data and array sizes
If the 1st inner set of braces encloses 10 numbers, only the first 10 elements of the 1st row are affected. The last 2 elements in that row are then initialized by default to 0. If there are too many numbers enclosed in that inner set of braces, then it is an error, the numbers do not get shoved into the next row.
{3}We could omit the interior braces and just retain the 2 outermost braces. As long as we have the right number of entries, the effect is the same. If we are short of entries, however, the array is filled sequentially, row by row, until the data runs out. Then the remaining elements are initialized to 0.
For example:
-------------------------------------------------------------------------------------------------------------------
int sq[2][3] = {{5, 6}, {7, 8}};
-------------------------------------------------------------------------------------------------------------------
The result will be: {{5, 6, 0}, {7, 8, 0}}.
-------------------------------------------------------------------------------------------------------------------
int sq[2][3] = {5, 6, 7, 8};
-------------------------------------------------------------------------------------------------------------------
The result will be: {{5, 6, 7}, {8, 0, 0}}.
2.More Dimensions
(1)Everything we have said about 2-dimensional arrays can be generalized to 3-dimensional arrays and further.
(2)We can visualize a 3-dimensional array as a stack of data tables. The other way to think of it is as an array of arrays of arrays.
(3)Typically, we would use 3 nested loops to process a 3-dimensional array, 4 nested loops to process a 4-dimensional array, and so on.
三、Pointers and Arrays
1.Array notation is simply a disguised use of pointers.
(1)An array name is also the address of the 1st element of the array.
For example:
If flizny is an array, the following is true:
-------------------------------------------------------------------------------------------------------------------
flizny == &flizny[0]; // name of array is the address of the 1st element
-------------------------------------------------------------------------------------------------------------------
(2)flizny and &flizny[0] are BOTH constants because they remain fixed for the duration of the program. But they can be assigned as values to a pointer variable, and we can change the value of a variable.
2.Take a look at the pnt_add.c program:
(1)We can see that the 2nd line prints the beginning addresses of the 2 arrays, and the next line gives the result of adding 1 to the address, and so on. But the result is not what we imagined because in the result, the number of the address are not separated by 1 but other numbers.
(2)Our system is addressed by individual bytes, but type short uses 2 bytes and type double uses 8 bytes. When we say "add 1 to a pointer", C adds 1 storage unit. For arrays, that means the address is increased to the address of the next element, not just the next byte.
(3)The reason why we have to declare the sort of object to which a pointer points: the address is not enough because the computer needs to know how many bytes are used to store the object. (This is true even for pointers to scalar variables; otherwise, the *pt operation to fetch the value would not work correctly.)
3.What is meant by pointer-to-int, pointer-to-float or pointer-to-any other data object:
(1)The value of a pointer is the address of the object to which it points. How the address is represented internally is hardware dependent.
[1]byte addressable == each byte in memory is numbered sequentially.
[2]The address of a large object, such as type double variable, typically is the address of the 1st byte of the object.
(2)Applying the * operator to a pointer yields the value stored in the pointed-to object.
(3)Adding 1 to the pointer increases its value by the size, in bytes, of the pointed-to type.
4.We have the following equalities:
-------------------------------------------------------------------------------------------------------------------
dates + 2 == &date[2] // same address
*(dates + 2) == dates[2] // same value
-------------------------------------------------------------------------------------------------------------------
5.The C language standard describes array notation in terms of pointers, which means that it defines ar[n] to mean *(ar + n).
6.The indirection operator (*) binds more tightly (that is, has higher precedence) than +.
7.Take a look at the day_mon3.c program:
(1)days is the address of the 1st element of the array, days + index is the address of element days[index], and *(days + index) is the value of that element, just as days[index] is.
(2)Is there an advantage to writing the program this way? Not really. The compiler produces the SAME code for either.
四、Functions, Arrays, and Pointers
1.Using Pointer Parameters
(1)Suppose you want to write a function that operates on an array, what would the prototype be?
As the name of an array is the address of its 1st element, so the actual argument of it, being the address of an int, should be assigned to a formal parameter that is a pointer-to-int:
-------------------------------------------------------------------------------------------------------------------
int sum(int * ar); // corresponding prototype
-------------------------------------------------------------------------------------------------------------------
[1]The information that sum() gets from this argument is the address of the 1st element of the array and it says nothing about the number of elements in the array.
[2]We have a couple of choices of how to get that information to the function.
{1}The 1st choice is to code a fixed array size into the function.
For example:
-------------------------------------------------------------------------------------------------------------------
int sum(int * ar) // corresponding definition
{
int i;
int total = 0;
for (i = 0; i < 10; i++) // assume 10 elements
total += ar[i]; // ar[i] the same as *(ar + i)
return total;
}
-------------------------------------------------------------------------------------------------------------------
{2}A more flexible approach is to pass the array size as a 2nd argument.
For example:
-------------------------------------------------------------------------------------------------------------------
int sum(int * ar, int n) // more general approach
{
int i;
int total = 0;
for (i = 0; i < n; i++) // use n elements
total += ar[i]; // ar[i] the same as *(ar + i)
return total;
}
-------------------------------------------------------------------------------------------------------------------
Here, the 1st parameter tells the function where to find the array and the type of data in the array, and the 2nd parameter tells the function how many elements are present.
[3]In the context of a function prototype or function definition header, and only in that context, you can substitute int ar[] for int * ar.
(2)Declaring Array Parameters
[1]Prototypes allow we to omit a name, all 4 of the following prototypes are equivalent:
{1}int sum(int *ar, int n);
{2}int sum(int *, int);
{3}int sum(int ar[], int n);
{4}int sum(int [], int);
[2]We cannot omit names in function definitions, so, for definitions, the following 2 forms are equivalent:
-------------------------------------------------------------------------------------------------------------------
int sum(int * ar, int n)
{
// code goes here
}
-------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------
int sum(int ar[], int n);
{
// code goes here
}
-------------------------------------------------------------------------------------------------------------------
(3)Take a look at the sum_arr1.c program:
[1]Note that the size of marbles is 40 bytes because marbles contains 10 ints, each 4 bytes, for a total of 40 bytes.
[2]The size of ar is just 8 bytes because ar is not an array itself, it is a pointer to the 1st element of marbles. (Other systems might use a different number of bytes.) Marbles is an array, ar is a pointer to the 1st element of marbles, and the C connection between arrays and pointers lets you use array notation with the pointer ar.
(4)A function working on an array needs to know where to start and stop.
(5)Another way to describe the array is by passing 2 pointers, with the 1st indicating where the array starts (as before) and the 2nd where the array ends.
(6)The function can alter the value of the pointer itself, making it point to each array element in turn.
(7)Take a look at the sum_arr2.c program:
[1]The pointer start begins by pointing to the 1st element of marbles, so the assignment expression total +=*start adds the value of the 1st element (20) to total. Then the expression start++ increments the pointer variable start so that it points to the next element in the array. Because start points to type int, C increments the value of start by the size of int.
[2]The pointer variable end actually points to the location after the final element in the array. When C allocates space for an array, a pointer to the 1st location after the last element of the array is a valid pointer.
[3]Although C guarantees that the pointer marbles + SIZE is a valid pointer, it makes no guarantees about marbles[SIZE], the value stored at that location, so a program should not attempt to access that location.
[4]We can also condense the body of the loop to 1 line:
-------------------------------------------------------------------------------------------------------------------
total += *start++;
-------------------------------------------------------------------------------------------------------------------
The unary operators * and ++ have the same precedence but associate from right to left. This means the ++ applies to start, not to *start. That is, the pointer is incremented, not the value pointed to.
[5]Different situations
{1}If the program used *++start, the order would be increment the pointer, then use the value pointed to.
{2}If the program used (*start)++, however, it would use the value of start and then increment the value, not the pointer. That would leave the pointer pointing to the same element, but the element would contain a new number.
2.Comment: Pointers and Arrays
(1)Using array notation makes it more obvious that the function is working with arrays. Also, array notation has a more familiar look to programmers versed in other languages. Other programmers might be more accustomed to working with pointers and might find the pointer notation more natural.
(2)As far as C goes, the 2 expressions ar[i] and *(ar+i) are equivalent in meaning. Both work if ar is the name of an array, and both work if ar is a pointer variable. However, using an expression such as ar++ only works if ar is a POINTER variable.
(3)Pointer notation, particularly when used with the increment operator, is closer to machine language and, with some compilers, leads to more efficient code.
五、Pointer Operations
1.Take a look at the ptr_ops.c program:
(1)The basic operations that can be performed with or on pointer variables:
[1]Assignment == assign an address to a pointer.
{1}Note that the address should be compatible with the pointer type. That is, we cannot assign the address of a double to a pointer-to-int, at least not without making an ill-advised type cast.
[2]Value finding (dereferencing) == use the * operator to give the value stored in the point-to location.
[3]Taking a pointer address.
{1}Like all variables, a pointer variable has an address and a value. The & operator tells you where the pointer itself is stored.
[4]Adding an integer to a pointer == we can use the + operator to add an integer to a pointer or a pointer to an integer. In either case, the integer is multiplied by the number of bytes in the pointed-to type, and the result is added to the original address.
[5]Incrementing a pointer == makes it move to the next element of the array.
{1}Note that the address of ptr1 itself remains the original one. After all, a variable does not move around just because it changes value.
[6]Subtracting an integer from a pointer
{1}The pointer has to be the 1st operand and the integer value the 2nd operand. The integer is multiplied by the number of bytes in the pointed-to type, and the result is subtracted from the original address.
{2}The result of subtraction is undefined if it lies outside of the array into which the original pointer points, except that the address one past the end element of the array is guaranteed to be valid.
[7]Decrementing a pointer
{1}We can use both the prefix and postfix forms of the increment and decrement operators.
[8]Differencing
{1}We find the difference between 2 pointers to elements that are in the same array to find out how far apart the elements are. The result is in the same units as the type size.
[9]Comparisons
{1}We can use the relational operators to compare the values of 2 pointers, provided the pointers are of the same type.
(2)There are 2 forms of subtraction. We can subtract 1 pointer from another to get an integer, and we can subtract an integer from a pointer and get a pointer.
(3)The computer does not keep track of whether a pointer still points to an array element. But the effect of incrementing or decrementing a pointer beyond these limits is undefined. Also, we can dereference a pointer to any array element. However, even though a pointer to 1 past the end element is valid, it is not guaranteed that such a one-past-the-end pointer can be dereferenced.
(4)Dereferencing an Uninitialized Pointer
[1]Do not dereference an uninitialized pointer.
For example:
-------------------------------------------------------------------------------------------------------------------
int * pt; // an uninitialized pointer
*pt = 5; // a terrible error
-------------------------------------------------------------------------------------------------------------------
pt is uninitialized, and it has a random value, so there is no knowing where the 5 will be placed. Creating a pointer only allocates memory to store the pointer itself, it does not allocate memory to store data.
六、Protecting Array Contents
1.Using const with Formal Parameters
(1)The usual rule == pass quantities by value unless the program needs to alter the value, in which case we pass a pointer.
(2)For arrays, we MUST pass a pointer. If a function passed an array by value, it would have to allocate enough space to hold a copy of the original array and then copy all the data from the original array to the new array. It is much quicker to pass the address of the array and have the function work with the original data.
(3)The reason C ordinarily passes data by value is to preserve the integrity of the data. If a function works with a copy of the original data, it will not accidentally modify the original data. But, because array-processing functions do work with the original data, they CAN modify the array.
(4)If a function's intent is that it not change the contents of the array, use the keyword const when declaring the formal parameter in the prototype and in the function definition.
For example:
-------------------------------------------------------------------------------------------------------------------
int sum(const int ar[], int n); /* prototype */
int sum(const int ar[], int n) /* definition */
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
-------------------------------------------------------------------------------------------------------------------
If we accidentally use an expression such as ar[i]++, the compiler can catch it and generate an error message, telling you that the function is attempting to alter constant data.
(5)Using const this way does NOT require that the original array be constant, it just says that the function has to treat the array AS THOUGH it were constant.
(6)In general, if we write a function intended to modify an array, do not use const when declaring the array parameter. If we write a function not intended to modify an array, do use const when declaring the array parameter.
(7)Take a look at the arf.c program:
[1]Here, show_array function displays an array and mult_array function multiplies each element of an array by a given value. As the 1st function should not alter the array, it uses const, and as the 2nd function intends to modify the array, it does not use const.
[2]Note that both functions are type void. The mult_array() function does provide new values to the dip array, but not by using the return statement.
2.More About const
(1)const can be used to create symbolic constants, which can be accomplished by #define directive. But const additionally lets us create cosntant arrays, constant pointers, and pointers to constants.
(2)Pointers to constants cannot be used to change values
For example:
-------------------------------------------------------------------------------------------------------------------
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates; // pd points to beginning of the array
-------------------------------------------------------------------------------------------------------------------
[1]The 2nd line of code declares that the type double value to which pd points is a const, which means that we cannot use pd to change pointed-to values:
-------------------------------------------------------------------------------------------------------------------
*pd = 29.89; // not allowed
pd[2] = 222.22; // not allowed
rates[0] = 99.99; // allowed because rates is not const
-------------------------------------------------------------------------------------------------------------------
[2]Whether we use pointer notation or array notation, we are NOT allowed to use pd to change the value of pointed-to data. However, because rates was NOT declared as a constant, we can still use rates to change values.
[3]And we can make pd point somewhere else.
For example:
-------------------------------------------------------------------------------------------------------------------
pd++; /* make pd point to rates[1] -- allowed */
-------------------------------------------------------------------------------------------------------------------
[4]A pointer-to-constant is normally used as a function parameter to indicate that the function will NOT use the pointer to change data.
For example:
-------------------------------------------------------------------------------------------------------------------
void show_array(const double *ar, int n);
-------------------------------------------------------------------------------------------------------------------
[5]Rules about pointer assignments and const
{1}First, it is valid to assign the address of either constant data or non-constant data to a pointer-to-constant.
For example:
-------------------------------------------------------------------------------------------------------------------
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
const double * pc = rates; // valid
pc = locked; // valid
pc = &rates[3]; // valid
-------------------------------------------------------------------------------------------------------------------
{2}Only the addresses of non-constant data can be assigned to regular pointers.
For example:
-------------------------------------------------------------------------------------------------------------------
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
double * pc = rates; // valid
pnc = locked; // NOT valid
pnc = &rates[3]; // valid
-------------------------------------------------------------------------------------------------------------------
After all, we could use the pointer to change data that was supposed to be constant.
{3}Using const in a function parameter definition not only protects data, it also allows the function to work with arrays that have been declared const.
{4}An attempt to modify const data, such as locked, using a non-const identifier, results in undefined behavior.
[6]More possible uses of const
{1}We can declare and initialize a pointer so that it cannot be made to point elsewhere. The trick is the placement of the keyword const.
For example:
-------------------------------------------------------------------------------------------------------------------
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
double * const pc = rates; // pc points to beginning of the array
pc = &rates[2]; // not allowed to point elsewhere
*pc = 92.99; // ok -- changes rates[0]
-------------------------------------------------------------------------------------------------------------------
Such a pointer can still be used to change values, but it can point only to the location originally assigned to it.
{2}Use const twice to create a pointer that can neither change where it is pointing nor change the value to which it points.
For example:
-------------------------------------------------------------------------------------------------------------------
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * const pc = rates;
pc = &rates[2]; // not allowed
*pc = 92.99; // not allowed
-------------------------------------------------------------------------------------------------------------------
七、Pointers and Multidimensional Arrays
1.Pointers to Multidimensional Arrays
(1)Suppose we have this declaration:
-------------------------------------------------------------------------------------------------------------------
int zippo[4][2]; /* an array of arrays of ints */
-------------------------------------------------------------------------------------------------------------------
[1]zippo[0] is itself an array of 2 ints, so zippo[0] has the same value as &zippo[0][0], the address of its 1st element, an int. In short, zippo[0] is the address of an int-sized object, and zippo is the address of a 2-int-sized object.
[2]zippo and zippo[0] differ, because zippo refers to an object 2 ints in size, and zippo[0] refers to an object 1 int in size.
[3]*zippo represents the value of its 1st element, zippo[0], but zippo[0] itself is the address of an int. It is the address &zippo[0][0], so *zippo is &zippo[0][0]. **zippo equals *&zippo[0][0], which reduces to zippo[0][0], an int. In short, zippo is the address of an address and must be dereferenced twice to get an ordinary value. An address of an address or a pointer of a pointer is an example of double indirection.
(2)Take a look at the zippo1.c program:
[1]The address of the 2-dimensional array, zippo, and the address of the 1-dimensional array, zippo[0], are the same. Each is the address of the corresponding array's 1st element, and this is the same numerically as &zippo[0][0].
[2]The name of a 2-dimensional array has to be dereferenced twice to get a value stored in the array. This can be done by using the indirection operator (*) twice or by using the bracket operator ([]) twice. (It also can be done by using 1 * and 1 set of [].)
(3)How to declare a pointer variable pz that can point to a 2-dimensional array such as zippo?
[1]The type pointer-to-int does not suffice, it is compatible with zippo[0], which points to a single int. Actually, pz must point to an array of 2 ints, not to a single int.
[2]We can do this:
-------------------------------------------------------------------------------------------------------------------
int (* pz)[2]; // pz points to an array of 2 ints
-------------------------------------------------------------------------------------------------------------------
This statement says that pz is a pointer to an array of 2 ints. Why use parentheses? [] has a higher precedence than *.
[3]A declaration such as:
-------------------------------------------------------------------------------------------------------------------
int * pax[2]; // pax is an array of 2 points-to-int
-------------------------------------------------------------------------------------------------------------------
We apply the brakcets first, making pax an array of 2 somethings. Next, we apply the *, making pax an array of 2 pointers. Finally, use the int, making pax an array of 2 pointers to int. This declaration creates 2 pointers to single ints while the original version uses parentheses to apply the * first, creating 1 pointer to an array of 2 ints.
(4)Take a look at the zippo2.c program:
We can represent individual elements by using array notation or pointer notation with either an array name or a pointer:
zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] == *(*(pz + m) + n)
2.Pointer Compatibility
(1)The rules for assigning 1 pointer to another are tighter than the rules for numeric types.
For example:
-------------------------------------------------------------------------------------------------------------------
int n = 5;
double x;
int * p1 = &n;
double * pd = &x;
x = n; // implicit type conversion
pd = p1; // compile-time error
-------------------------------------------------------------------------------------------------------------------
(2)Restrictions mentioned above extend to more complex types.
For example:
-------------------------------------------------------------------------------------------------------------------
int * pt;
int (*pt)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; // a pointer to a pointer
-------------------------------------------------------------------------------------------------------------------
Then we have the following:
-------------------------------------------------------------------------------------------------------------------
pt = &ar1[0][0]; // both pointer-to-int
pt = ar1[0]; // both pointer-to-int
pt = ar1; // not valid
pa = ar1; // both pointer-to-int[3]
pa = ar2; // not valid
p2 = &pt; // both pointer-to-int *
*p2 = ar2[0]; // both pointer-to-int
p2 = ar2; // not valid
-------------------------------------------------------------------------------------------------------------------
The nonvalid assignments all involve 2 pointers that do not point to the same type. For instance, the type of pa is a pointer that points to an array with 3 int while the first element of ar2 is an array with 2 int, so the address type of ar2 is a pointer that points to an array with 2 int.
(3)Multiple indirection is tricky.
For example:
-------------------------------------------------------------------------------------------------------------------
int x = 20;
const int y = 23;
int * p1 = &x;
const int * p2 = &y;
const int ** pp2;
p1 = p2; // not safe -- assigning const to non-const
p2 = p1; // valid -- assigning non-const to const
pp2 = &p1; // not safe -- assigning nested pointer types
-------------------------------------------------------------------------------------------------------------------
Although assigning a const pointer to a non-const pointer is not safe (because you could use the new pointer to alter const data), the code would compile, perhaps with a warning, the effect of executing the code is undefined. But assigning a non-const pointer to a const pointer is okay, as you are dealing with just 1 level of indirection.
For example:
-------------------------------------------------------------------------------------------------------------------
p2 = p1; // valid
-------------------------------------------------------------------------------------------------------------------
But such assignments no longer safe when you go to 2 levels of indirection.
For example:
-------------------------------------------------------------------------------------------------------------------
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // allowed, but const qualifier disregarded
*pp2 = &n; // valid, both const, but sets p1 to point at n
*p1 = 10; // valid, but tries to change const n
-------------------------------------------------------------------------------------------------------------------
(4)C const and C++ const
[1]C and C++ use const similarly but not identically.
{1}C++ allows using a const integer value to declare an array size and C is more restrictive.
{2}C++ has stricter rules about pointer assignments.
For example:
-------------------------------------------------------------------------------------------------------------------
cosnt int y;
const int * p2 = &y;
int * p1;
p1 = p2; // error in C++, possible warning in C
-------------------------------------------------------------------------------------------------------------------
In C++ we are not allowed to assign a const pointer to a non-const pointer. In C, you can make this assignment, but the behavior is undefined if we try to use p1 to alter y.
3.Functions and Multidimensional Arrays
(1)Write a function to deal with 2-dimensional arrays.
[1]One possibility is to use a for loop to apply a 1-dimensional array function to each row of the 2-dimensional array.
For example:
-------------------------------------------------------------------------------------------------------------------
int junk[3][4] = { {2, 4, 5, 8}, {3, 5, 6, 9}, {12, 10, 8, 6} };
int i, j;
int total = 0;
for (i = 0; i < 3; i++)
total += sum(junk[i], 4); // junk[i] -- 1-dimensional array
-------------------------------------------------------------------------------------------------------------------
If junk is a 2-dimensional array, junk[i] is a 1-dimensional array, which we can visualize as being 1 row of the 2-dimensional array. But this approach loses track of the column-and-row information.
[2]Suppose we need the funciton should have the row and column information available. This can be accomplished by declaring the right kind of formal variable so that the function can pass the array properly.
Then the array junk is an array of 3 arrays of 4 ints which means junk is a pointer to an array of 4 ints. We can declare a function parameter of this type like this:
-------------------------------------------------------------------------------------------------------------------
void somefunction( int (* pt)[4] );
-------------------------------------------------------------------------------------------------------------------
Alternatively, if (and only if) pt is a formal parameter to a function, we can declare it as this:
-------------------------------------------------------------------------------------------------------------------
void somefunction( int pt[][4] );
-------------------------------------------------------------------------------------------------------------------
The 1st set if brackets is empty. The empty brackets identify pt as being a pointer.
(2)Take a look at the array2d.c program:
[1]ar is used in the same fashion as junk is used in main(), because ar and junk are the same type: pointer-to-array-of-four-ints.
[2]The following declaration will not work properly:
-------------------------------------------------------------------------------------------------------------------
int sum2(Int ar[][], int rows); // faulty declaration
-------------------------------------------------------------------------------------------------------------------
The compiler converts array notation to pointer notation. For compiler to evaluate this, it needs to know the size object to which ar points. The declaration:
-------------------------------------------------------------------------------------------------------------------
int sum2(int ar[][4], int rows); // valid declaration
-------------------------------------------------------------------------------------------------------------------
It says that ar points to an array of 4 ints (hence, to an object 16 bytes long on our system), so ar+1 means "add 16 bytes to the address." With the empty-bracket version, the compiler would not know what to do.
[3]We can also include a size in the other bracket pair, as shown here, but the compiler ignores it:
-------------------------------------------------------------------------------------------------------------------
int sum2(int ar[3][4], int rows); // valid declaration, 3 ignored
-------------------------------------------------------------------------------------------------------------------
(3)In general, to declare a pointer corresponding to an N-dimensional array, we must supply values for all but the leftmost set of brackets:
-------------------------------------------------------------------------------------------------------------------
int sum4d(int ar[][12][20][30], int rows);
-------------------------------------------------------------------------------------------------------------------
The 1st set of brackets indicates a pointer, whereas the rest of the brackets describe the type of data object being pointed to, as the following equivalent prototype:
-------------------------------------------------------------------------------------------------------------------
int sum4d(int (*ar)[12][20][30], int rows); // ar a pointer
-------------------------------------------------------------------------------------------------------------------
八、Variable-Length Arrays (VLAs)
1.Variable-length array == allows us to use variables when dimensioning an array.
For example:
-------------------------------------------------------------------------------------------------------------------
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // a VLA
-------------------------------------------------------------------------------------------------------------------
2.Restrictions of VLAs
(1)They need to have the automatic storage class, which means they are declared either in a function without using the static or extern storage class modifiers or as function parameters.
(2)We cannot initialize them in a declaration.
(3)Under C11, VLAs are an optional feature rather than a mandatory feature, as they were under C99.
(4)VLAs do not change size. The term variable in variable-length array does not mean that we can modify the length of the array after we create it. Once created, a VLA keeps the same size.
3.How to declare a function with a 2-dimensional VLA argument
(1)We can declare it like this:
-------------------------------------------------------------------------------------------------------------------
int sum2d(int rows, int cols, int ar[row][cols]); // ar a VLA
-------------------------------------------------------------------------------------------------------------------
[1]The 1st 2 parameters (rows and cols) are used as dimensions for declaring the array parameter ar. Because the ar declaration uses rows and cols, they have to be declared BEFORE ar in the parameter list.
[2]The C99/C11 standard says we can omit names from the prototype, but in that case, we need to replace the omitted dimensions with asterisks:
-------------------------------------------------------------------------------------------------------------------
int sum2d(int, int, int ar[*][*]); // ar a VLA, names omitted
-------------------------------------------------------------------------------------------------------------------
(2)How to define the function
-------------------------------------------------------------------------------------------------------------------
int sum2d(int rows, int cols, int ar[rows][cols]);
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < cols, c++)
tot += ar[r][c];
return tot;
}
-------------------------------------------------------------------------------------------------------------------
[1]Aside from the new function header, the only difference from the classic C version of this function (in the array2d.c program) is that the constant COLS has been replaced with the variable cols. The presence of the variable length array in the function header is what makes this change possible.
[2]Having variables that represent both the number of rows and columns lets us use the new sum2d() with any size of 2-dimensional array of ints.
[3]It requires that a C compiler that implements the VLA feature.
[4]This VLA-based function can be used with either traditional C arrays or with a variable-length array.
(3)Take a look at the vararr2d.c program:
[1]A VLA declaration in a function definition parameter list does not actually create an array. The VLA name really is a pointer, which means a function with a VLA parameter actually works with the data in the original array, therefore, it has the ability to modify the array passed as an argument.
[2]Dynamic memory allocation == we can specify the size of the array while the program is running.
[3]Static memory allocation == the size of the array is determined at compile time.
(4)const and array sizes
[1]For C90, we cannot use a const symbolic constant when declaring an array (probably). The size has to be given by an integer constant expression, which can be a combination of integer constants, such as 20, sizeof expressions, and a few other things, none of which are const.
[2]For C99/C11, we can use a const symbolic constant when declaring an array, if the array could otherwise be a VLA.
九、Compound Literals
1.Suppose we want to pass an array argument.
(1)Before C99, we could pass an array, but there was no equivalent to an array cosntant.
(2)C99 added compound literals.
[1]Literals == constants that are not symbolic.
[2]For arrays, a compound literal looks like an array initialization list preceded by a type name that is enclosed in parentheses.
For example, an ordinary array declaration is like this:
-------------------------------------------------------------------------------------------------------------------
int diva[2] = {10, 20};
-------------------------------------------------------------------------------------------------------------------
For example, we can create a nameless array containing the same 2 int values:
-------------------------------------------------------------------------------------------------------------------
(int [2]{10, 20}) // a compound literal
-------------------------------------------------------------------------------------------------------------------
{1}Type name == what we would get if we removed diva from the earlier declaration, leaving int [2] behind.
[3]We can omit the array size from a compound literal, and the compiler will count how many elements are present.
-------------------------------------------------------------------------------------------------------------------
(int []){50, 20, 90} // a compound literal with 3 elements
-------------------------------------------------------------------------------------------------------------------
[4]Because these compound literals are nameless, we cannot just create them in 1 statement and then use them later. Instead, we have to use them somehow when we make them.
{1}One way is to use a pointer to keep track of the location.
For example:
-------------------------------------------------------------------------------------------------------------------
int * pt1;
pt1 = (int [2]) {10, 20};
-------------------------------------------------------------------------------------------------------------------
This literal constant is identified as an array of ints. Like the name of an array, this translates to the address of the 1st element, so it can be assigned to a pointer-to-int.
{2}Another thing we could do with a compound literal is pass it as an actual argument to a function with a matching formal parameter.
For example:
-------------------------------------------------------------------------------------------------------------------
int sum(const int ar[], int n);
...
int total3;
total3 = sum((int []){4, 4, 4, 5, 5, 5}, 6);
-------------------------------------------------------------------------------------------------------------------
(3)Take a look at the flc.c program:
[1]A compound literal is a means for providing values that are needed only temporarily.
[2]Block scope == its existence is not guaranteed once program execution leaves the block in which the compound literal is defined, that is, the innermost pair of braces containing the definition.