C++ Zero To One 0.002

数组, 指针数组, 指向指针的指针的数组, ….

在逛Cplusplus Forum - Beginner的时候发现了一个看上去看简单的问题

How do I receive two numbers from the keyboard, save them in an array of two element?

第一次尝试

void f0() {     // ERROR: 先存放再输入不会修改数组中的数据
int a, b;
int iarr[2] = {a, b};
std::cin >> a >> b; // 不合理:先存放, 再输入
std::cout << iarr[0] << ' ' << iarr[1] << std::endl;
// 4197661 0
}

我想得太简单了。不能在标准输入中, 直接对数据进行修改。

下面给出的答案

1,

void f1() {
int iarr[2] = {};
std::cin >> iarr[0] >> iarr[1]; // 直接输入到数组中
}

2,

void f2() {
int a, b;
std::cin >> a >> b; // 先输入, 再存放
int iarr[2] = {a, b};
}

但是我仍然考虑我思路实现的可能性?

对已经放置在数组中的值进行修改的思路如何实现?

第一个想到的就是指针。

void f3() {  // BUG: 为什么不能通过数组中保存的指针修改数组??
int *pa, *pb;
int* iarr[2] = {pa, pb};
int tmp_a, tmp_b;
std::cin >> tmp_a >> tmp_b; // 存放到临时变量
pa = &tmp_a; pb = &tmp_b;
std::cout << "*pa = " << *pa << std::endl; // *pa = 1
std::cout << *(*iarr) << std::endl;
std::cout << *(iarr[0]) << ' ' << *(iarr[1]) << std::endl;
}
$ ./a.out
1
2
*pa = 1
Segmentation fault (core dumped)

很气, 我得出结论。不能通过数组中保存的指针修改数组。

数组作为函数形参

接着, 我想起之前在函数中通过数组改变过值, 便想要实现如下任务:
创建一个函数, 输入一个保存指向整数的指针的数组, 修改索引n位置的指针所指向的值, 返回

void revise_value_int(int* arr, int n) {
// 索引为n指向的整数递增1
(*(arr+n)) += 1;
}

void call_revise() {
int iarr[] = {0, 1, 2};
revise_value_int(iarr, 2);
std::cout << "iarr[2] = " << iarr[2] << std::endl;
}

call_revise();

可以改变。但是仔细一想, 我仅仅只是在数组中存放了整数。

函数形参为存放指针的数组

接着,我改变了数组,用于存放指针

int* pa, *pb, *pc;
int ptrArr[] = {pa, pb, pc};

编译时就出现了错误,.error: invalid conversion from ‘int’ to ‘int’ [-fpermissive]
因为现在数组保存着的是指向整型的指针, 因此数组的类型应该是 int\

修改

int* pa, *pb, *pc;
int* ptrArr[] = {pa, pb, pc};

写修改指针所指向整数的函数, 注意, 这个形参类型是int**, ptrArr是指向指针的指针, 调用数组, 实际上是调用指向数组首元素的指针。

void revise_value_ptr(int** ptrArr, int n) {
// 索引为n的指向的指针所指向的整数递增1
(**(ptrArr+n))+=1;
}

Segmentation fault (core dumped)

再次编译

Segmentation fault (core dumped)

出现 Segmentation fault (core dumped) 的原因, 参考[http://blog.sina.com.cn/s/blog_4b9eab320100uivj.html]

1 内存访问越界

a) 由于使用错误的下标,导致数组访问越界

b) 搜索字符串时,依靠字符串结束符来判断字符串是否结束,但是字符串没有正常的使用结束符

c) 使用strcpy, strcat, sprintf, strcmp, strcasecmp等字符串操作函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。

2 多线程程序使用了线程不安全的函数。

3 多线程读写的数据未加锁保护。

对于会被多个线程同时访问的全局数据,应该注意加锁保护,否则很容易造成core dump

非法指针

a) 使用空指针
b) 随意使用指针转换。一个指向一段内存的指针,除非确定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,否则不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易因为bus error而core dump.

堆栈溢出

不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。

非法指针

我的代码是:

int* pa, *pb, *pc;
int* ptrArr[] = {pa, pb, pc};

这里我使用了非法的指针, 这个指针没有指向任何

修改,

void call_revise() {
int a = 1, b = 2, c = 3;
int* pa = &a, *pb = &b, *pc = &c;
int* ptrArr[] = {pa, pb, pc};
another_revise_value_ptr(ptrArr, 2);
std::cout << "**(ptrArr) = " << **(ptrArr) << '\n'
<< "**(ptrArr+1) = " << **(ptrArr+1) << '\n'
<< "**(ptrArr+2) = " << **(ptrArr+2) << std::endl;
}

call_revise()

编译得到

**(ptrArr)   = 1
**(ptrArr+1) = 2
**(ptrArr+2) = 4

解决最开始问题并总结

修改后的代码

void f4() {     // DEBUG: 先存放再输入不会修改数组中的数据
int a, b;
int* pa = &a, int* pb = &b; // 注意: 不要定义空指针
int* iarr[2] = {pa, pb}; // 注意数组的类型是数组中元素的类型
std::cin >> a >> b; // 也能合理:先存放, 再输入
std::cout << **iarr << ' ' << **(iarr+1) << std::endl; // 解引用
// 4197661 0
}

不要定义空指针

如这样

int* pa;

即使要定义也要

int *pa = 0;
int *pb = nullptr;

尽量让指针指向某个对象

int *pa = &a;

数组的类型

数组的类型是数组中元素的类型

int* iarr[2] = {pa, pb};        // 类型为 int*

解引用数组

int* iarr[2] = {pa, pb};        // 如果数组这样定义
iarr; // iarr是指向数组首个元素的指针
*iarr; // 因为pa是指针, 所以解引用数组头指针, 得到pa地址
**iarr; // 再次解引用得到pa指向的int

标准输出它们得到

iarr = 0x7ffed5caa560
*iarr = 0x7ffed5caa548
**iarr = 1

更多的尝试

数组中存放对象

template <typename T>
class Name {
char* _c; int _length; int _capacity;
public:
Name(char* c, int length): _c(c), _length(length) {}
char* getName() const;
int getLength() const;
};

template <typename T>
char* Name<T>::getName() const {
return _c;
}

template <typename T>
int Name<T>::getLength() const {
return _length;
}



void class_array() {
char c1[] = {'t', 'i', 'm'};
char c2[] = {'h', 'u'};
int length1 = 3, length2 = 2;
Name<int> name1(c1, length1), name2(c2, length2);
Name<int> nArr[] = {name1, name2};
int size = 2;
print_instance_data_in_array(nArr, size);
}

void f() {
char c[] = {'t', 'i', 'm'};
int length = 3;
Name<int> n(c, length);
}

void print_instance_data_in_array(Name<int>* nArr, int size) {
for (int i = 0; i < size; i++) {
std::cout << "Name = ";
for (int j = 0; j < nArr[i].getLength(); j++) {
std::cout << *((nArr[i].getName())+j);
}
std::cout << "\n";
}
}
Name = tim
Name = hu

面向对象可以让我从无尽的指针中抽身出来, 关注相互之间的联系。

数组中存放长度不同的数组

void f_array() {
int a = 3, b = 5, c = 7;
int* pa = &a, *pb = &b, *pc = &c;
int _size_pa = 1, _size_pb = 1, _size_pc = 1;
int* pArr1[] = {pa}; // pArr1数组存放pa, pb指针
int _size_pArr1 = 1;
int* pArr2[] = {pa, pb, pc}; // pArr2数组存放pa, pb, pc指针
int _size_pArr2 = 3;
int* pArr3[] = {pa, pc};
int _size_pArr3 = 2;
int size_pArr[] = {1, 3, 2};
int** ppArr[] = {pArr1, pArr2, pArr3}; // ppArr存放3个指针数组
int _size_ppArr = 3;
int size_ppArr[] = {3};
int* size_arr[] = {size_ppArr, size_pArr};
// convert_size_structor(size_arr);
print_arr_b(ppArr);
}

void print_arr_b(int*** ppArr) {
// 解引用并打印ppArr数组中依次数组中的pa, pb, pc
std::cout << "a0 = " << ***ppArr<< " in pArr1[0]" << '\n'
<< "b0 = " << **(*(ppArr+1)+1)<< " in pArr2[1]" << '\n'
<< "c0 = " << **(*(ppArr+2)+1)<< " in pArr3[1]" << '\n'
<< std::endl;
}

对于简单的数组, 打印储存不同长度数组的数组, 需要更多的数据结构知识, 而且不同的算法还有好坏, 可见数据结构的重要性. 这部分的打印实现, 留作日后我数据结构解引用后的小试牛刀。

感想

即使从一个很小很简单的问题入手, 也能对自己有所提高, 计算机科学没有简单, 复杂是由简单构成的, 计算机科学承认复杂, 计算机科学尝试破译复杂。

常量成员函数

class MyClass {
int x = 0;
public:
int c = 5;
MyClass(int v) :x(v) {}
const int& get() const {return x;} // 常量成员函数在是否为常量上可以重载
int& get() {return --x;}
};

声明时引用符号总最靠近变量名

为什么可以对a.get()赋值

get()返回的是引用, 引用可作为左值, 且如果非常量引用的话, 该值可以被修改。

在是否为成员函数上重载后如何匹配?

成员函数在是否为常量函数上可以重载。如果对象实例化成const类型, 则匹配常量成员函数。

const int& get() const {return x;} 为什么返回类型需要const?

常量函数返回数据成员, 编译器会将数据成员声明成const类型;

什么是数据成员?

MyClass类public中定义新成员测试是否为数据成员。

//..
int a = 5
const int& get() const {return --a;} // 错误:改变a的值,

改变a的值无法通过编译, a为数据成员。

在class中声明的, 无论是私有的还是公有的数据, 都是成员数据。如果在常量函数中返回, 会常量函数会被声明成const类型。

template基础用法

typename T 和 class T的区别

在模板参数列表中, typename和class没有什么不同。 但每个参数类型前必须加上typename 或者 class

<<C++ Primer 5th>> P580 模板与泛型编程

特殊化模板

template <typename T>
class mycontainer {
T element;
public:
mycontainer(const T e) : element(e) {}
T increase() {return ++element;} // 递增element
T getElement() const {return element;} // 返回element数据
};

对char类型, 有特定的方法uppercase()大写化字母

template <>
class mycontainer<char> {
char element;
public:
mycontainer(const char e): element(e) {}
char increase() {return ++element;} // 递增char
char getElement() const {return element;} // 返回element数据
char uppercase() {
if (element <= 'z' && element >= 'a') { // 是小写字母
element += 'A' - 'a'; return element;
} // 返回元素
std::cout << "element " << element << "is not lower" << std::endl;
return element;
}
};

格式如下

template <typename T> class mycontainer {...}
template <> class mycontainer<char> {...}