C++ 学习笔记

Posted xiaolongtuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 学习笔记相关的知识,希望对你有一定的参考价值。

想到一条写一条,源自对书籍 《Effective C++》(第三版)与《C++ Primer》(第五版)的一些阅读感悟。

1、.h与.cpp

.h文件可以理解为声明或接口(类似Java里面的Interface),其内容一般为各种数据类型的定义以及函数的声明,而一般在对应同名的.cpp文件中编写具体实现,在.cpp文件里要include对应的头文件:
e.g. test.h

#define int my_int;
void sayhello();

test.cpp

#include <iostream>
#include "test.h"
void sayhello()

    std::cout<< "hello~" << std::endl;

头文件与同名源文件的关系,下面转载百度文库上的一段
1)系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。
#include <xxx.h>
2)用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到C++安装目录(比如VC中可以指定和修改库文件查找路径,Unix和Linux中可以通过环境变量来设定)中查找,最后在系统文件中查找。
#include “xxx.h”
问题:头文件如何来关联源文件?
这个问题实际上是说,已知头文件“a.h”声明了一系列函数,“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢?
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。比如偶现在看到偶们公司的源代码,.cpp文件由.cc文件替代了。
在Turbo C中,采用命令行方式进行编译,命令行参数为文件的名称,默认的是.cpp和.h,但是也可以自定义为.xxx等等。
谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行“文件包含处理”:将file2.c的全部内容复制到#include “file2.c”处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么—-因为#include预处理就是完成了一个“复制并插入代码”的工作。
编译的时候,并不会去找b.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp或c.cpp中用#include “a.h”实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o或.obj文件),目标文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件中。
在Unix下,甚至可以不在源文件中包括头文件,只需要在makefile中指名即可(不过这样大大降低了程序可读性,是个不好的习惯哦^_^)。在VC中,一般情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。
通常,编译器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示“redefined”。

2、.h文件防止重复编译(头文件安全保护)

在编写头文件时要注意安全保护,如果在程序中多次引入同一个头文件,在编译时会简单将同一个头文件连接起来,但是这样就会导致数据类型的重复定义。
解决办法使用 预编译命令 #ifndef
预编译变量有两种状态,已定义与未定义, #ifndef 是 if not def(如果未定义)的缩写,通过判断是否已定义标识的方式来判断是否已经引入某个头文件。标识一般为文件名全大写,”.”变下划线,有的额外在前后加上下划线。
e.g. header.h

#ifndef HEADER_H    //或者 _HEADER_H_
#define HEADER_H
...
#endif

这样在编译时如果当前头文件已经被某个文件引入,那么宏 HEADER_H 肯定已经存在,从而不会再重复定义 headre.h中的各种数据类型、函数定义等。
所有头文件统一使用这种定义方式就可保证所有头文件只会被编译一次,而且在每个要使用该头文件的地方都可以随意引入而不用担心重复编译,最常见的就是我们经常在多个地方引用 string.h 。
较新版的编译器支持一种命令 #pragma once 。只要在头文件最开始加入这个命令,也可以达到这种效果,不过由于一些较老的编译器不支持该命令,很多时候会配合 #ifndef 来使用。
另外关于这个作为标识的预编译变量,注意不要与已知常用头文件重复(比如string这种),在大型项目编写时这点也要格外注意。

3、指针常量的定义 const的用法

对于指针常量的定义很容易让人迷糊,其实并不高深,const有两种,一种为“顶层”一种为“底层”,如果关键字const出现在星号左边,表示被指物是常量(而指针本身可以修改指向别的对象,但不能通过指针修改原对象),即C++Primer中定义的“底层”const;如果出现在星号右边,表示指针本身是常量(不能修改指针使其指向别的对象,但可以通过该指针修改被指的对象),即“顶层”const。
例如:

char str[10] = "hello";
char str2[10] = "world";
//定义指向常量的指针cpstr,被指的内容不可(通过cpstr)更改
const char *cpstr = str;    

//定义常量指针指向对象,不可修改pcstr使之指向其他对象,但可通过pcstr修改对象内容
char *const pcstr = str;    

*cpstr = 'H';   //错误,尝试给常量赋值
*pcstr = 'H';   //正确,str变为"Hello"

cpstr = str2;   //正确,cpstr重新指向str2
pcstr = str2;   //错误,尝试修改常量指针pcstr

//PS:   str[10] = "hello" 本身当然可以通过str来修改
//      定义字符串常量方法见下面

所以,一般在定义常量指针时要同时保证指针本身以及被指对象都不可(通过指针)修改:

const char *const cstr = "hello";

ps:在C++中这样定义明显较繁琐,故推荐使用string来定义字符串常量:

const std::string cstr2 = "hello";

引申:关于指针、常量、类型别名,例如:

typedef char* pstring;
const pstring cstr = 0; //cstr是指向char的常量指针,不可修改
const pstring *ps;      //ps是一个指针,它的对象是指向char的常量指针

上述两条声明语句其基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。pstring实际上是指向char的指针,因此,const pstring 就是指向char的常量指针,而非指向常量字符的指针。

典型错误理解:

const char *cstr = 0;   //是对const pstring cstr的错误理解

这种情况下实际被指的对象变为常量,而 cstr 本身可以自由修改使之指向其他对象。

4、左值与右值

C++中左值右值的概念源自C语言,原本是为了帮助记忆:左值可以位于赋值语句的左侧,而右值则不能。
但在C++中,二者区别就没那么简单了:当一个对象呗用作右值是,用的是对象的值(内容“值”);当对象呗用作左值的时候,用的是对象的身份(身份“值”)。

不同的运算符的作用对象不同,有的要左值,有的要右值,而其返回结果也有差异。一个重要的原则(提醒:有一例外)是在需要右值的地方可以用左值来代替,但是不能把右值当成左值使用,当一个左值被当成右值使用时,实际使用的是他的内容(值)。

可以这么形象理解,可以将“值”理解为房子,左值是有门牌号的房子,而右值没有,所以当我们执行赋值等操作时,因为左值有门牌号,所以我们能通过门牌号来对房子里的东西进行修改,而右值没有门牌号,我们根本就找不到它,更无从对其修改。而因为左值有门牌号,当我们将左值作为右值使用时,我们当然也能通过门牌号来取到房子里面的东西。
PS:字面值与算术表达式都是右值

简单总结(只列出部分,有待进一步完整)

运算符名称符号作用对象输出对象
赋值运算符=(左侧为)左值左值
取地址符&左值右值(一个指向该运算对象的指针)
解引用*左值左值
下标运算符[]左值左值
递增递减++ --左值左值
逻辑与关系运算! < <= >= > == != && ||右值右值
箭头运算符->左值右值
点运算符.左值/右值若作用于左值则结果为左值,若作用于右值则结果为右值
逗号运算符,左值/右值与右侧对象类型相同
条件运算符?:左值/右值若expr1与expr2都能转换成统一左值类型则结果为左值,否则为右值

PS:因为左值可以作为右值使用,所以针对右值的运算符当然也可作用于左值,比如最常见的 bool flag = true; if(!flag)

5、声明符 ‘*’ 与 ‘&’

大家都知道在C/C++里定义指针与引用时要用到这两个“声明符”,作为Java转来的我本来之前C学的就很渣,所以经常对指针与引用等的定义犯迷糊。
其实说来你只需要记住一点: 无论指针还是引用都是一种 内置类型 ,而要定义这两种内置类型需要使用这两个“声明符”,“声明符”只是对外声明:我是XX类型。

记住这点在读代码时就很容易理解指针或引用的定义以及使用:

int i = 0;
int *pi;
int &ri = i;    //引用在定义时必须绑定一个被引用的对象
pi = &i;
r = 3;
*pi = 5;

const *foo(const int *lhs, const int &rhs)/*some codes*/
const &bar(const int *lhs, const int &rhs)/*some codes*/

在对 指针/引用 进行定义时,其基本形式是:
被 指向/引用 的对象类型 声明符 标识符
就如int *pi;以及int &ri = i;
pi 来说,定义pi时用 ‘*’ 号声明了pi是一个指针,那么他指向什么呢?再往前看,找到了int,那么很自然就能理解:pi是一个指向整型变量的指针。
而使用时就如同一般的变量一样,不再需要声明符,比如上面代码中的pi = &i;r = 5;(这里‘&’不是声明符而是取地址的运算符)

PS: 在语句 *pi = 5; 中 ‘*’ 号是解引用的运算符,如果对指针本身进行操作,比如使指针指向别的变量:pi = &j; ,而如果要对指针所指变量进行操作就要用到解引用符 ‘*’。

同样,在定义函数时声明符的作用也很明显,放在函数名称之前就声明了这个函数的返回类型是指针或引用,比如const *foo(...)...
可以将形参理解为为临时变量,所以才在参数列表那儿用与变量定义相同的方式,在形参标识符之前加上声明符。

PS:值传参与引用传参
在使用值传递的函数在被调用时,会使用实参的值来对形参进行初始化,这时形参可以认为是实参的复制品,对形参的任何修改都不会作用到实参上,在使用较简单的内置类型(且不需要对原实参进行修改)时常使用值传递,而在很多情况下对一些内置类型以及用户自定义的类型的初始化/复制都是很耗时的,故常使用引用传参。

在使用引用传参的函数在被调用时,不会对形参进行初始化,对形参的任何修改都不会作用到实参上,这种情况下,要限制对实参的修改我们可以使用关键字const。

6、构造函数的编写不要想当然

最近在写个小东西时遇到了一个很蛋疼的问题,涉及自定义类的初始化与构造函数

class String

    String(const char*);
    String(String& str);
    ...

int main()

    String a="hello";

就是这么简单的一段却遇到问题,编译报错找不到对应的转换函数从 String到String& 或 从String 到const char*的转换

按照这个报错,实际对a进行初始化编译器是这么来的:
先将”hello”自动构造为一个String对象,不妨叫他tmp,之后再将tmp作为构造函数的参数,找对应的构造函数来构造a,但是明显是有问题的,不存在从 String到String& 或 从String 到const char*的转换,所以找不到对应的构造函数。

但是为什么会出现这种“两步走”,为什么不直接调用 String(const char*) 来对a进行初始化呢?

问题的元凶就在 String(String& str);
编译器不知道怎么处理String a = “hello”
有两种路径:
一种直接String(const char*)
另一种用copy构造函数
将 “hello” 自动转换为String对象(正是因为有String(const char*)
所以存在这样的构造方式,参考int a = 0.5),再用copy构造函数构造a
按照报错的逻辑是选择了使用copy构造函数的方式来编译,但是为何没有最佳匹配到String(const char*)?
我只能猜测是这个String(String&)把构造函数表搞乱了,编译器在处理String a = “hello”是蒙了,那么到底是怎么产生这个问题呢?我们回头慢慢理理。

首先系统默认合成的copy构造函数的形式是String(const String&),而我们这里给的构造函数是String(String&),严格来说其参数列表是不一样的,并没有重载掉系统自动合成的copy构造函数。
然而又存在非const到const的自动转换,所以我猜测是这个原因把构造函数表搞乱了,明明可以简单用String(const char*)来初始化a的,但是因为编译器蒙了,改成了“两步走”。

修正的方法十分简单,就是将 String(String& str); 改为 String(const String& str); 从而系统会正确的最佳匹配到String(const char*)来初始化a,而不会匹配到copy构造函数。

从这个简单的例子可以看出对构造函数的编写不要想当然,牢记系统默认的构造函数的形式,不然出的问题很难定位到原因是在构造函数上(比如例子中是报不存在对应的转换)。

PS: 不存在接受参数类型为String的copy构造函数,因为函数调用的机理是用实参初始化形参,然而如果形参为String类型则会递归无限调用copy构造函数,所以编译器本身就强制了不存在这样的copy构造函数,copy构造函数的基本形式是String(const String&)。

以上是关于C++ 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

算法笔记_208:第六届蓝桥杯软件类决赛真题(Java语言A组)

OpenCV C++案例实战十《车牌号识别》

C++ C++ Primer 基础知识笔记

C++ C++ Primer 基础知识笔记

C++ C++ Primer 基础知识笔记

MVC学习笔记:入门