Thinking in C++
像任何人类语言一样,C++提供了一种表达思想的方法。如果这种表达方法是成功的,那么当问题变得更大和更复杂时,该方法会明显地表现出比其他方法更容易和更灵活的优点。
C++编程思想第一卷这本书,我理解它的目的旨在帮助C程序员更好的理解C++为什么出现,如何从C过渡到C++,C++与C的不同之处在哪。哈哈,它像一本C++导读,感觉这本书是不是翻译成C++的前世今生更好,看过后再去看其他书可能才会理解其它规则或技巧的原因是什么。关于每个知识点的细致的用法讲解,可以看看其他书籍。
C++是一种能提高生产效率的工具。生产效率就是“更少的人在更短的时间完成更难的事”。考虑语言还应考虑:运行效率(代码臃肿速度减慢)、安全性(纠错能力)、可维护性(易改易纠错)。
程序开发的原则是:首先让它能用,然后再优化。
Chapter 3 C++中的C
1.为什么使用指针?
-
指针是用来保存地址的变量,使用地址可以去修改地址里面放的值,但是为什么要通过另一个变量来管理修改一个变量呢?
-
为了能在函数内部改变“外部对象”,基本作用->函数的形参是地址(即指针)。传引用是另一种传递地址的方式。
-
为了获得更多的编程技巧,?
-
-
引用和指针有什么不同?见Tips1
- 引用和指针能完成的工作是一样的,可以说只使用指针也能完成所有的工作,但函数传引用比函数传指针在语法分析树上更加清晰。可以认为是语法糖。
2. 作用域
作用域就是变量的有效范围,从哪开始从哪销毁?从定义的那一点开始,到最邻近的第一个闭括号结束。嵌套:所处的作用域可以访问外层的作用域的变量。
3. 变量的定义
C++随时可以定义,可在即将使用的时候进行定义并进行初始化,定义并初始化是C++编程的一般原则,防止出错;而C语言要求在作用域的开始处就定义全部的变量,以便编译器可以为他们分配空间。
4. 指定存储空间的分配
-
全局变量:定义于所有函数体外部。不受作用域影响,生命周期是整个程序结束,在其他文件中也是可见的,只需要使用extern来告诉编译器在哪找这个变量。
-
局部变量:定义于一个函数体内。局部变量默认auto。生命周期是函数体,进入函数生成,离开函数消失。
- 寄存器变量:关键字register,告诉编译器“尽快的访问这个变量”。但仅仅是暗示,并不一定有效。避免使用
-
静态变量:关键字static,主要作用是改变生命周期和可见性。
-
外部变量:extern,在一个文件的开头用extern声明一个变量,告诉编译器其他文件中有它的定义。也是用于控制变量的可见性。
-
常量:C与C++中的const有什么区别?在C中,对待const和对待变量是一样的,但区别在于他是一个不可改变的变量。如果在不同文件里定义了相同名字的const变量就报错。连接器会发生错误冲突信息。在C++中,常量应当是存储在常量/全局区的。C++比C应用的更好。具体可以看Chapter8里详细的讲解。
-
volatile变量:告诉编译器,“不知道何时会发生改变”,不允许对变量进行任何优化,比如连续多次赋值,如果是普通的变量就会选择只赋值最后一次(因为编译器会找冗余的读入),而volatile限定后就不会进行这样的优化。 在读代码控制之外的某个变量的值时,例如读通信硬件中寄存器的值。
5. C++的显示转换
转换的意思是:告诉编译器,忘记类型检查,把它当做其他类型。这样就为系统引入了一个漏洞,并阻止编译器对其进行类型报错。类型转换只是用来解决特殊问题。不应过多应用类型转换。使用C++类型转换的好处可以让程序员在出错的时候可以快速定位在哪里出错了,C语言就不够明显。
-
static_cast:用于做明确定义的转换。即通常用于基本类型的转换。有这样几种类型的转换(不是说用来干这个,而是通过用法进行了分类)
-
典型的非强制转换:
//Typical castless conversions: int i=0x7fff; long l; float f; l=static_cast<long>(i); f=static_cast<float>(i);
-
窄化变换(信息丢失)
//Narrowing Conversions char c =static_cast<char> (i);
-
使用void* 的强制转换
//Forcing a convertion from void* void* voidptr = &i;
-
隐式类型转换和类层次的静态定位(就是突出强调一下某处发生了静态类型转换)
//突出强调作用,便于查找错误 void function(int){} int main() { double d; function(static_cast<int>(d)); }
-
-
dynamic_cast:用于父类子类之间的转换,进行上下行转换。上行转换即派生类转换成基类,是安全的。下行即基类转换成派生类,是不安全的。可以用于类型安全的下行转换。
-
const_cast:去除const属性或用于去除volatile属性。
-
reinterpret_cast:最危险。例如用于int类型和指针之间的转换。You can do anything you want , and you take risks.
更具体的用法可以参考其他书,例如C++ Primer Plus.
6. typedef命名别名
typedef的作用(八股文里有说很多,这里直说两点):
-
少打字
typedef unsigned long ul;
-
将指针用typedef的例子
int* x,y; //x是int*,但y是int,因为*绑定右边 typedef int* IntPtr; IntPtr x,y; //二者都是int*类型
-
为struct定义别名,可以少打一个struct(一般是用于C语言中)
//在C语言中 struct Person { int age; double weight; } int main() { struct Person dad;//需要加上struct } //如果应用了typedef typedef struct { int age; double weight; }Person; typedef struct Person { int age; double weight; }; int main() { Person son; Person* dad; }
7. int main
如果想向一个程序传进参数,可以由两种形式:
int main(int argc, char* argv[]) //第一个是参数个数 第二个是参数列表 是一个字符指针 数组(数组里存放是的字符串的地址)
int main(int argc, char** argv) //这个用来存放指针的数组也采取指针的形式 而不再是一个数组
8. 函数指针
-
定义函数指针: 看到复杂的定义要从中间开始、向外扩展(先右再左,再右再左…)
void (*functionPtr) (); //functionPtr是一个->...<-指针,指向一个->形参为空的 <-返回类型是void的函数。 void *functionPtr(); //如果不给*functionPtr加括号,就变成了一个返回类型是void*的函数。
-
例子:
void * (*(*fp1)(int))[10]; //(*(*fp1)(int))标准形式 fp1是一个指向函数的指针,该函数的入口参数是int,返回的是一个指针 //返回的是一个什么类型的指针? 一层一层看 由里向外 //void* [10] 一个装有10个void*的数组的指针
float (*(*fp2)(int,int,float)) (int) //(*(*fp2)(int,int,float)) fp2是一个指向函数的指针,该函数有int int float三个参数,并且返回一个指针 //返回一个什么类型的指针? //返回的是一个指向函数的指针,该函数的参数是int,返回值是float
Tips1: 指针和引用的区别
在Chapter3中提过,引用能实现的事指针都能实现,引用会看起来更加干净,意义更清晰。指针和引用都是内存地址的概念。但区别在于,指针是一个内存地址的实体,而引用是一个别名。别名的意思是什么?就是我可以通过这个名字而不是指针来获取该指针里的内容。
- 引用的本质是指针常量。一经初始化后所指向的地址就不可改变。不能再指向别人。编译的时候是将“引用变量名-引用对象的地址“添加到符号表。
- 指针在编译的时候是将“指针变量名-指针变量的地址”添加到符号表。指针的地址可以更改。分为const和非const。const不可以改变,非const可以改变。
- 引用不能为空,指针可以为空。
- sizeof(引用)是对象的大小,sizeof(指针)就是指针本身的大小。
- 传指针(或者说传地址)的方式本质也是传值,只不过传递的值是指针的地址,而传引用的本质是传地址,直接对地址的内容进行操作。
Chapter 4 数据抽象
第四章介绍的是数据抽象,书中提到,C++的主要设计目标之一就是使库用起来更容易,换句话说,C的库用起来不那么容易,所以第一节创建了一个C的mini库,以便向我们展示C的库是什么样的、存在哪些弊端。由此引出C++在数据抽象上是怎么做的,有哪些好处。在C++中,对象就是变量,最好的定义应该是“一块存储区”,对象必须有唯一的标识,就是一个唯一的地址。是一块空间,能够存储数据和对这些数据进行的操作。数据和函数捆绑在一起就是封装。
所以C语言在做数据抽象(设计类)的时候有哪些明显的问题呢?这里我主要总结了书中提到的以下几点:
- C语言版创建对象的时候必须主动调用
Initialize()
、销毁时主动调用CleanUp()
。
C库的问题是必须向Client Programmer说明初始化函数和清除函数的重要性,如果没有调用会出现很多问题,但程序员总是容易忘记的。会存在内存泄漏memory leak等风险。对应的,C++的类中我们有构造函数和析构函数帮我们解决这个问题,自动调用,我们只要提前写好就可以。在对象的定义的那一刻调用构造函数,在超出函数的作用域时调用析构函数。
//以用C语言创建一个mini动态数组的库为例(只展示部分)Page 110
//Clib.h
typedef struct CStashTag
{
int size;
int next;
unsigned char* storage;//unsigned char*是C编译器支持的最小存储单位,一般是一个字节。
} Cstash;
void Initialize(CStash* s, int size);
void CleanUp(CStash* s);
void add(CStash* s, const void* element);
//CLib.cpp
void Initialize(CStash* s, int nums)
{
s->size = nums;
s->next = 0;
}
void CleanUp(CStash* s)
{
if(s->storage != 0)
delete []s->storage;
}
- 形参冗余复杂,显得笨拙。
从以上的几个函数可以看到,我们必须硬性的结构的地址传进每个函数,通常放在第一个形参的位置。想想C++,就不需要我们做这件事,编译器秘密的帮我们做了(那就是this指针)。
- 在C中,使用库的最大障碍之一就是名字冲突name clashes。
如果从两个厂商买了两个库,比如刚好两个库的Initialize()
和CleanUp()
重名了,就麻烦了。为什么呢?这涉及C语言的对函数的编译过程。如果同一个处理单元(一个处理单元指的是一个文件吗?)中,同时包含了这两个含同名函数的库,标准C会报错,说声明函数有两个不同的参数表,类型不匹配;如果没有放进同一个处理单元也会发生错误,连接器会出现问题,好的连接器会报告出现命名冲突,有些编译器会直接按照函数出现的次序直接调用第一个函数。因为C使用单个名字空间,要查一个函数时,编译器就会在主表中进行查找。C语言的函数签名只取决于函数名字,与里面的参数没有关系,这也是为什么C语言不支持函数重载。鉴于此,一般厂商会在自己的所有函数上写上独特的字符串。
Tips1: 不要在头文件中使用using
原因是,在头文件中使用using指令相当于失去了对该文件命名空间的保护,这么做,导致只要有包含当前头文件的文件,都可以不加限定的使用此命名空间的内容,不就失去了对这个命名空间的保护吗?失去了意义。