目录

目录

【学习笔记】C++ Primer Plus(第6版)【基础篇】

目录

【学习笔记】C++ Primer Plus(第6版)【基础篇】



这本书可以说是久仰大名了,但一直没有深入学习。一方面是没有时间,另一方面则是从学生时代的课堂学习转到看书自学中间还是需要个过渡的。这段时间刚度过考试周能喘口气,又为实习就业等未来感到焦虑,就看看书提升下自我吧。

本来我学习是没有做笔记的习惯的,会就是会不会就是不会。但大学读了几年,愈发感到力不从心。要学的知识太多太杂,很多东西你学完当时掌握得很好,转过头回来就忘了个七七八八。这种感觉很痛苦,就像是游戏回坑结果新手村都出不去。最后还是妥协了,做点笔记吧,忘了什么查查笔记总比翻一本几百页的大部头方便。

第一次边学边作笔记,我也不知道怎么弄比较好,摸索着来吧。这次我打算按目录分章分类,记下相对陌生的知识点方便日后查阅,更多是一种自用的功能性的索引的感觉。


这一章更多是一个科普的内容,感觉有空看看也还行。学干货这章就先跳过吧。


这部分就是hello world,大部分内容都会。

头文件命名约定可以看看,了解一下c和c++头文件的区别。

2.1_1

命名空间主要是用来区分不同的版本。例如A和B都写了一个函数func(),就可以用A::func(),B::func()来区别。 using namespace std;是一种偷懒的做法,现在我已经改掉了这个习惯,直接使用std::cin等。另一种可行的方法是using std::cin,这样也可以直接使用cin,相对using整个命名空间要安全些。此外,还可以将using语句放在特定的函数或结构体定义内来限制范围。

这一部分没什么重要的内容。主要是面向初学者的专业名词解释。

注意:C++程序应当为程序中使用的每个函数提供原型。

我之前一直将原型称为声明,因为原型的作用就和声明一样,告诉编译器有这么个东西。不过我一般不写原型,而是直接在用到函数之前把函数的定义写好在前面。这或许并不是一个好的编码习惯,需要改正。 再复习下原型和定义的区别: 2.4_1


3.1_1

可对类型名或变量名使用sizeof运算符。对类型名(如 int)使用sizeof运算符时,应将名称放在括号中;但对变量名(如n_short)使用该运算符,括号是可选的:

cout << "int is " << sizeof (int) << " bytes .\n" ;
cout << "short is " << sizeof n_short << " bytes.\n" ;

头文件climits定义了符号常量来表示类型的限制。 3.1_2 3.1_3

C++使用前一(两)位来标识数字常量的基数。

  • 如果第一位为1~9,则基数为10(十进制);因此93是以10为基数的。
  • 如果第一位是0,第二位为1~7,则基数为8(八进制);因此042的基数是8,它相当于十进制数34。
  • 如果前两位为0x或0X,则基数为16(十六进制);

头文件iostream提供了控制符dechexoct,分别用于指示cout以十进制、十六进制和八进制格式显示整数。

一种实现可以同时支持一个小型基本字符集和一个较大的扩展字符集。8位char可以表示基本字符集,另一种类型wchar_t (宽字符类型)可以表示扩展字符集。wchar_t类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。这种类型与另一种整型(底层(underlying)类型)的长度和符号属性相同。对底层类型的选择取决于实现,因此在一个系统中,它可能是unsigned short,而在另一个系统中,则可能是int。 cin和 cout将输入和输出看作是char流,因此不适于用来处理wchar_t类型。iostream头文件的最新版本提供了作用相似的工具——wcinwcout,可用于处理wchar_t流。另外,可以通过加上前缀L来指示宽字符常量和宽字符串。下面的代码将字母Р的wchar_t版本存储到变量bob中,并显示单词tall 的wchar_t版本:

wchar_t bob = L'P';
l/ a wide-character constant
wcout << L"tall" << endl; ll/ outputting a wide-character string

在支持两字节 wchar_t 的系统中,上述代码将把每个字符存储在一个两个字节的内存单元中。

C++11新增了类型char16_tchar32_t,其中前者是无符号的,长16位,而后者也是无符号的,但长32位。C++11使用前缀u表示char16_t字符常量和字符串常量,如uC’和u"be good";并使用前缀U表示char32_t常量,如U’R’和U"dirtyrat”。

char16_t ch1 = u'q' ;
l / basic character in 16-bit form
char32_t ch2 = U'\U0000222B'; // universal character name in 32-bit form

与wchar_t一样,char16_t和char32_t也都有底层类型——一种内置的整型,但底层类型可能随系统而已。

const相比于#define的优点:

  • 首先,它能够明确指定类型。
  • 其次,可以使用C++的作用域规则将定义限制在特定的函数或文件中。
  • 第三,可以将const用于更复杂的类型,如第4章将介绍的数组和结构。

此外,在C++(而不是C)中可以用const值来声明数组长度。

可以从cfloatfloat.h头文件中关于有效位数和指数范围等的限制。

在默认情况下,像8.24和2.4E8这样的浮点常量都属于double类型。 如果希望常量为float类型,请使用f或F后缀。 对于long double类型,可使用l或L后缀(由于l看起来像数字1,因此L是更好的选择)。 下面是一些示例:

1.234f  // a float constant
2.45E20F // a float constant
2.345324E28 // a double constant
2.2L  // a long double constant

书里只简单讲了讲优先级和结合性的概念。这里放一张微软文档里的表,这张表的内容很全,方便查表。 顺便吐槽,这表里有机翻啊,除法翻译成部门,赋值翻译成转让。。。我用md的表格语法贴上来,改掉上述翻译错误。

运算符说明运算符替代方法
第 1 组优先级,无关联性
范围解析::
第 2 组优先级,从左到右关联
成员选择(对象或指针)或 ->
数组下标[]
函数调用()
后缀递增++
后缀递减
类型名称typeid
常量类型转换const_cast
动态类型转换dynamic_cast
重新解释的类型转换reinterpret_cast
静态类型转换static_cast
第 3 组优先级,从右到左关联
对象或类型的大小sizeof
前缀递增++
前缀递减
二进制反码~compl
逻辑“非”!not
一元求反-
一元加+
Address-of&
间接寻址*
创建对象new
销毁对象delete
强制转换()
第 4 组优先级,从左到右关联
指向成员的指针(对象或指针)或 ->*
第 5 组优先级,从左到右关联
乘法*
除法/
取模%
第 6 组优先级,从左到右关联
加法+
减法-
第 7 组优先级,从左到右关联
左移«
右移»
第 8 组优先级,从左到右关联
小于<
大于>
小于或等于<=
大于或等于>=
第 9 组优先级,从左到右关联
相等==
不相等!=not_eq
第 10 组优先级,从左到右关联
位与&bitand
第 11 组优先级,从左到右关联
位异或^xor
第 12 组优先级,从左到右关联
位或|bitor
第 13 组优先级,从左到右关联
逻辑与&&and
第 14 组优先级,从左到右关联
逻辑或||or
第 15 组优先级,从右到左关联
条件逻辑? :
赋值=
乘法赋值*=
除法赋值/=
取模赋值%=
加法赋值+=
减法赋值-=
左移赋值«=
右移赋值»=
按位“与”赋值&=and_eq
按位“与或”赋值|=or_eq
按位“异或”赋值^=xor_eq
引发表达式throw
第 16 组优先级,从左到右关联
逗号,

3.4_2 在进行列表初始化(list-initiallization),即使用{}进行初始化(C++11)时,不允许缩窄(narrowing),即变量的类型可能无法表示赋给它的值。

const int code = 66;
int x = 66;
char c1{31325}; // narrowing, not allowed
char c2 = {66}; // allowed because char can hold 66
char c3{code}; // ditto
char c4 = {x}; // not allowed,x is not constant
x = 31325;
char c5 = x; // allowed by this form of initialization

需要注意的一些规则:

  • 只有在定义的时候才能初始化
  • 如果只对数组一部分初始化,则编译器把其他元素设置为0
  • 如果初始化数组是方括号[]内为空,则C++编译器将计算元素个数。 一般不推荐这样做。不过,在初始化const char[]时可以这么做,不用去数字符串长度。

以下是C++11新增的一些特性:

  • 初始化数组时可省略等号
  • 可不在大括号内包含任何东西,这将把所有元素设置为零
  • 列表初始化禁止缩窄转换,详见3.4 C++算术运算符-类型转换

cin.getline():该函数读取整行,使用回车键输入的换行符确定输入结尾。 第一个参数:存储输入行的数组的名称 第二个参数:读取的字符数。注意: 有一个多余空间用于存储’\0’,如下实际能存储的字符数为19

cin.getline(name,20);

cin.get():该函数在读入一行时参数和用法大致与getline()相同,区别是getline()读取并丢弃换行符,而get()将其留在输入队列中。 另一种用法是使用不带参数的cin.get(),它将读取下一个字符,因此可以用于处理换行符。 在混合输入数字和字符串时,就可以用这个方法处理掉多余的换行符。如:

(cin>>age).get()>>name;

这个问题我挺在意,但书上表示在之后章节再详细介绍,先mark一下

c中的char数组需要使用strcpy()进行复制,strcat()进行拼接。而c++的string类可以通过赋值进行复制,+运算符进行拼接。例如,将字符串1和2拼接后复制给3:

//C风格
strcpy(charr3, charr1);
strcat(charr3, charr2);
//C++风格
str3 = str1 + str2;

相比于c的数组操作,c++的类操作还不用担心长度溢出等问题。

cin.getline(charr, 20); //char数组读入一行
getline (cin , str) ; // string类读入一行

这里没有使用句点表示法,这表明这个getline()不是类方法。它将cin 作为参数,指出到哪里去查找输入。另外,也没有指出字符串长度的参数,因为string对象将根据字符串的长度自动调整自己的大小。 istream类因为引入在string类之前,所以没有考虑string类型。cin>>x在读入其他类型时,使用的是istream类的成员函数,而cin>>str使用的是string类的友元函数。关于友元函数将在11章详细介绍。

原始字符串将"()"用作定界符,并使用前缀R来标识原始字符串:

cout << R"(Jim "King" Tutt uses "\n" instead of endl. )" << ' \n ';
  • 输入原始字符串时,按回车键不仅会移到下一行,还将在原始字符串中添加回车字符。
  • 自定义定界符时,在默认定界符之间添加任意数量的基本字符,但空格、左括号、右括号、斜杠和控制字符(如制表符和换行符)除外。
  • 可将前缀R与其他字符串前缀结合使用,以标识wchar_t等类型的原始字符串。R放在前后都可以。

没什么需要注意的知识点。记一下匿名结构体吧。

可以省略名称,定义一种结构类型和这种类型的变量。

struct
{
    int x;
    int y;
}position;

这种结构体类型没有名称,因此无法创建这种类型的变量,只能在定义的同时声明变量。

类型:整形或枚举。定义方式:冒号+位数。

struct torgle_register
{
    unsigned int sN : 4; // 4 bits for sN value
    unsigned int : 4;    // 4 bits unused
    bool goodIn : 1;     // valid input (1 bit)
    bool goodTorgle : 1; // successful torgling
};

书上提到位字段常用于低级编程,也就是更接近硬件和底层,所以了解一下就行,可以使用位运算替代。

共用体(union) 是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。

这部分书上没提,稍微查了下,共用体的大小要大于最大的成员,并且对齐所有的成员。以下面这个共用体为例:

union U
{
    char s[9];
    int n;
    double d;
};

用运算符sizeof测试其大小为16。这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。

匿名共用体和上面讲的匿名结构体差不多。它的一种用法是作为结构体的成员变量,共用体的成员将成为相同地址处的结构体的变量。

struct widget
    {
        char brand[20];
        int type;
        union // anonymous union
        {
            long id_num;      // type 1 widgets
            char id_char[20]; // other widgets
        };
    };
    widget prize;
    if (prize.type == 1)
        cin >> prize.id_num;
    else
        cin >> prize.id_char;

如上,可以直接使用prize.id_num访问共用体内成员,不需要命名和定义共同体在通过共用体访问内部成员。

定义格式:enum 类型名 {枚举量[,枚举量]};,以下是一个示例:

enum spectrum {red , orange, yellow, green, blue, violet, indigo, ultraviolet};
  • 默认情况下枚举量的值从0开始类推。
  • 也可以通过赋值显式设置枚举量的值。
  • 指定的值必须是整数。
  • 如果只显式定义了部分枚举量的值,未被初始化的枚举量的值将比前一个大1。
  • 也可以创建多个值相同的枚举量。

每个枚举都有取值范围(range),通过强制类型转换,可以将取值范围内的任何整数值赋值给枚举变量,即使这个值不是枚举值。

取值范围的确定如下:

  • 上限: 大于最大值的最小的2的幂减1。
  • 下限:若最小值为0,则下限为0;否则,为上限相同方式加负号。 C++11增加了域内枚举(scoped enumeration),这将在第十章介绍。

两个基本运算符:取地址&解引用*

在c中,可以使用malloc()calloc()分配内存。

 void *malloc( size_t size );
 void *calloc( size_t num, size_t size );

calloc()内部其实就是用malloc实现的。 可以使用free()释放内存。

void free( void *ptr );

在C++中,使用new申请内存空间,delete释放内存空间。

pointer = new type;
pointer = new type( initializer );
pointer = new type[size];

delete p;
delete[] pArray;

书中在这里还提到了静态联编和动态联编的区别。

静态联编:

使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置。

动态联编:

使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应使用delete[]释放其占用的内存。

这一部分看起来或许很抽象,但在应用时很容易出错。

数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。

short tell[10];        // tell an array of 20 bytes
cout << tell << endl;  // displays &tell[0]
cout << &tell << endl; // displays address of whole array

从数字上说,这两个地址相同;但从概念上说,&tell[0](即tell )是一个2字节内存块的地址,而&tell是一个20字节内存块的地址。因此,表达式tell+1将地址值加2,而表达式&tell+2将地址加20。换句话说, tell是一个 short指针(*short ),而&tell是一个这样的指针,即指向包含20个元素的short 数组(short * )。

让我们看看这个short数组的指针类型:short (*)[20],乍一看似乎并不好理解。这是一个长为20的short数组的指针?还是一个short指针的数组,长为20?让我们接着看下去。 我们可以这样声明和初始化这种指针:

short (*pas)[20] = &tell;   //pas points to array of 20 shorts

如果省略括号,优先级规则将使得pas先与[20]结合,导致pas是一个short 指针数组,它包含20个元素,因此括号是必不可少的。其次,如果要描述变量的类型,可将声明中的变量名删除。因此, pas的类型为short(*)[20]。另外,由于pas被设置为&tell,因此*pas与tell等价,所以(*pas)[0]为tell数组的第一个元素。

这里通过优先级的区别回答了上述问题。我提供另一种理解思路,不一定对:

  • 如果没有括号,short与*结合,认为是short指针,再通过后面的[20]判定为指针数组。
  • 有括号时,先是short和[20]结合判定为short数组,再通过*判定为数组指针。

我再讲讲关于使用方括号[] 进行数组索引的问题。这个方括号其实就相当于解引用,比如a[5]就相当于\*(a+5)。理解了这个,一些直接使用指针的数组操作就更好理解了。

字符串指针和其他类型有所不同,它就是char *,而不是char (*)[20],因为字符串的长度是由后续地址中’\0’的位置决定的,而不是一开始被声明的。此外,在尝试输出字符串地址时,将会输出字符串的值:cout<<str;。如果需要输出字符串的地址,需要强制转换成其他类型的指针:cout<<(int*)str;

这部分其实看看书就好,我简单总结一下:

  • 自动存储就是在函数内部定义的变量,即局部变量,只在其所在的代码块内有效,离开代码块后内存被释放。
  • 静态存储的方式有两种,一种是在函数外定义,即全局变量;另一种是使用static关键字。静态存储的变量存在程序的整个生命周期,在程序结束后释放内存。
  • 动态存储就是使用newdelete申请和释放内存,管理了一个内存池。这种方式更自由,但也导致了更复杂的内存管理。

进一步总结一下分配位置和生命周期:

  • 自动存储分配在上,生命周期与所属函数的生命周期相同。
  • 静态存储是分配在数据段中的,其生命周期从程序开始执行到结束都存在。
  • 动态存储分配在自由存储区上,需要手动分配和释放内存,其生命周期由程序员控制。

没什么内容,看看指向指针数组的指针这种一堆*号嵌套怎么辨别吧。

int a;                    // int类型
int *b[3] = {&a, &a, &a}; // int指针数组
int **c = b;              // 指向int指针数组的指针

一种方式就是之前提到的,*和[]的转换。比如b,你把后面的[3]看成一个*,于是b的类型就为int**。从int**到int* []也是一样的,可以看出int**是一个int指针的数组。 书上还提到用auto自动获取类型:auto c=b,就不用纠结怎么写c的类型了。但这样编译是过了,还是不知道c到底是什么类型。另一种可行的方法是typeid(b).name(),我们只要输出一下,编译器就会告诉我们数据类型了。

简单介绍了模板类vectorarray,讲的比较浅。贴两个声明方式吧:

vector<typeName> vt (n_elem) ;
arrayetypeName, n_elem> arr;

它们的区别在于:vector长度可变,array长度固定。这是因为vector内部就是用new和delete来管理内存的,它的内存空间位于自由存储区和堆,而array的内存空间和常规数组一样位于栈或数据段中。 此外,vector和array和常规数组一样不检查下标越界的问题。为了安全可以使用at()成员函数,它将在运行期间捕获非法索引,同时程序默认中断。


都是基础,记点杂项。

通常,cout在显示 bool值之前将它们转换为int,但cout.setf (ios:: boolalpha)函数调用设置了一个标记,该标记命令cout显示 true和 false,而不是1和0。

对于内置类型,采用哪种格式不会有差别; 但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高。

都是基础,记点杂项。

头文件ctime(较早的实现中为time.h)首先定义了一个符号常量——CLOCKS_PER_SEC,该常量等于每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,可以得到秒数。或者将秒数乘以CLOCK_PER_SEC,可以得到以系统时间单位为单位的时间。其次,ctime将clock_t作为clock()返回类型的别名(参见本章后面的注释“类型别名”),这意味着可以将变量声明为clock_t类型,编译器将把它转换为long、unsigned int或适合系统的其他类型。

for(auto x:array):对数组(或容器类)的每个元素执行相同的操作。 for(auto &x:array):&表示x为引用变量,使可以修改数组内容。

看看书吧。这里记一下通过键盘来模拟文件尾条件。Unix:Crtl+D,Windows:Crtl+ZEnter

检测到EOF后,cin将两位(eofbit和failbit)都设置为1。可以通过成员函数eof( )来查看eofbit是否被设置;如果检测到EOF,则cin.eof()将返回 bool值 true,否则返回 false。同样,如果eofbit或failbit被设置为1,则 fail()成员函数返回 true,否则返回false。注意,eof()和fail()方法报告最近读取的结果;也就是说,它们在事后报告,而不是预先报告。因此应将cin.eof( )或cin.fail()测试放在读取后,程序清单5.18中的设计体现了这一点。它使用的是 fail(),而不是eof( ),因为前者可用于更多的实现中。

事实上,关于EOF的检测还有很多细碎的知识点,我就不一一列举了,掌握常用的方法就行。这部分内容更多是扩展一下视野让你知道为什么会这样,以及简单提及对象、原型和重载这些概念,对我作用不大。 5.2_1

这对我是一种很新鲜的写法,因为我一般直接用std::string数组或者二维char数组。这种方法仔细想想也确实可行。

const char *cities[5] = // array of pointers
    {                   // to 5 strings
        "Gribble city",
        "Gribbletown ",
        "New Gribble",
        "san Gribble",
        "Gribble vista"};

都是基础,略

有一点书上只提了一下,但我认为需要注意单独拿出来(看到后面才发现书上还是讲了这点,只不过举的除零的例子)。就是或运算符||和与运算符&&的判断是有顺序的,后续表达式对结果没有影响时会提前结束。这使得我们可以写出一些看上去有问题的逻辑表达式,因为逻辑运算符的本质就是写了个会提前返回的if-else表达式嵌套。例如我们判断可变数组里某个值是否为1(当然,这只是一个简单的例子,实际应用会比这复杂得多):

if(v[i] == 1)                 // exist risk
if(v.size() >= i && v[i] == 1) // more safe
/* equal to below
if(v.size() >= i)
{
    if(v[i] == 1)
        return true;
    else
        return false;
}
else
    return false;
*/

这种写法会先判断数组长度避免越界,再访问对应下标的值。如果下标越界,v.size() >= i的值为false,后面无论是true还是false都对结果没有影响,于是判断提前结束。即使下标越界,v[i] == 1语句并不会被执行,所以运行时不会出问题。

此外,C++中也可以用and,or、not作为逻辑运算符。c中要想这样做需要包含iso646.h头文件,不过我个人觉得没必要。

6.3_1

这个最好只用于简单条件判断,复杂的嵌套什么的可以写但没必要,不差那几行代码对吧,可读性比较重要。

注意:switch语句中的break不是必须的,你可以通过将多个case并在一起来提高代码复用。

书上提到使用枚举型变量,这是因为枚举型变量本质上就是给0,1,2这样的具体值起了个别名,这点我们在之前提到过。

break和continue很基础就不说了。 goto书上提了一句, 大多数情况甚至任何情况都尽量避免使用goto。我也见过goto存在即合理就是给你用的这样的观点。我的看法是如果使用前叙述的结构化语句很复杂麻烦,而goto能很好地提升代码可读性的话用用也未尝不可,没必要当洪水猛兽。

提到了一点错误处理,在本应输入数字是输入字符,可以先用cin.clear()重置cin,再while(cin.get()!=’\n’) 来读掉错误输入,最后要求用户重新输入。 我觉得这个看看就行不用认真记。后面肯定有专门的错误处理方法。这种就初学者为了用户交互用用。

一些简单的文件读写,详细的要看后面。临时简单用用还行。

写入文本文件:

  • 1.包含头文件 fstream。
  • 2.创建一个ofstream对象。
  • 3.将该ofstream对象同一个文件关联起来(ofstream.open(filename))。
  • 4.就像使用cout那样使用该ofstream对象。

在使用ofstream.open()是,如果文件不存在将被创建;如果文件已存在默认将丢弃原有内容重新写入。 使用完文件后记得用ofstream.close()将其关闭。

读取文本文件大致相似。可以使用ifstream.is_open()检测文件是否成功打开。

if(!fin.is_open())
    exit(EXIT_FAILURE);

如果文件被成功地打开,方法 is_open()将返回 true;因此如果文件没有被打开,表达式!inFile.isopen()将为true。函数 exit()的原型是在头文件cstdib 中定义的,在该头文件中,还定义了一个用于同操作系统通信的参数值EXIT_FAILURE。函数exit()终止程序。

\n\r什么的书上也提了下,感觉算那种暂时可以不了解但不能不知道的小知识,大部分时候都不用管但不知道什么时候就会因为这个出问题。这东西Windows和Linux还不一样,每次看到这类东西都想着要是能统一一下就好了,但也没啥可能,积重难返。

警告:Windows文本文件的每行都以回车字符和换行符结尾;通常情况下,C++在读取文件时将这两个字符转换为换行符,并在写入文件时执行相反的转换。有些文本编辑器(如Metrowerks CodeWarrior IDE编辑器),不会自动在最后一行末尾加上换行符。因此,如果读者使用的是这种编辑器,请在输入最后的文本后按下回车键,然后再保存文件。

在检测文件是否成功打开时,一种简单的方法是使用good(),该方法将在没有发生任何错误时返回true,也可以使用eof()(读到eof),fail()(读到eof、类型不匹配),bad()(文件受损、硬件故障等)来尝试确定具体原因。


主要还是讲函数原型,讲的太细反而看不出有什么重点。。。 函数和返回值类型不匹配时会自动转换什么的用过一两次就知道了。

了解一下形参(parameter)和实参(argument)的概念。形参是实参的副本,修改形参并不会影响实参。但如果你需要修改实参,则可以传递地址或者取引用,后面会讲。

主要就是第4章的数组的地址和指针类型里讲过的,数组名相当于第一个元素的地址。当然,他和真正的地址有一点区别,因为程序是将它标记成数组的,所以sizeof运算符和&地址运算符都返回的是整个数组的长度or地址。而进行函数传参时,它就失去了作为数组名的特性,仅仅是首元素地址了,所以我们通常需要额外的参数来说明数组长度

在只有一级间接关系时还好理解,到了二级就有点似懂非懂,看了蛮久才勉强明白。所以说指针这东西就是麻烦。这里直接贴结论,详细还是看书多思考一下:

注意:如果数据类型本身并不是指针,则可以将const 数据或非const 数据的地址赋给指向const 的指针,但只能将非const 数据的地址赋给非const指针。

  • 当const写在指针类型时,它表示指向常量的指针,指针指向对象的值不能改变,但指针的对象可以改变。
  • 当const写在指针类型时,它表示常量指针,指针指向对象的值可以改变,但指针的对象不能改变。

当然,也可以写两个const,使指针指向的对象和指向对象的值都不能改变。

做函数参数的二维数组长啥样?答案是int (*)[],这是类型名。例如一个3*4的二维数组arr,就是int (*arr)[4]。当然可以写int arr[][4],后者可读性更好但前者更利于你了解指针。又来了,C/C++的噩梦——指针。这个地方多一个括号少一个括号意思完全不一样,难怪不少人谈指针色变。

带括号时,int (*arr)[4],它表示指向长为4的int数组的指针;而不带括号的int *arr[4],它表示长为4的指向int类型指针的数组。是不是有点晕了(笑)。我们这样来看,带括号时,先是int与[4]结合,表示长为4的int数组,再看*号,就是指向这个数组的指针。而不带括号时,首先是int与*结合,表示指向int类型的指针,再看后面的[4],表示长为4的数组,这样就行了。

我之前想当然的移位一维数组是int *,二维数组就应该是int **,这是不对的,实际上后者表示的是指向int指针的指针,或者说可以作为int指针的数组。这是你可能就疑问了,每个一维数组是一个int指针,int指针的数组不就是二维数组了吗?这你就忽略了最重要的一个因素:长度。你没有指定一维数组的长度,这种定义就相当于连续的一维数组指针放在一起而没有对应的数组空间,你只能动态给它分配内存,和我们平常说的拥有连续内存空间的二维数组并不一样。举个例子:

// 静态分配内存
int arr[3][4];
// 动态分配内存
int **arr2;
arr2 = new int *[3];
for (int i = 0; i < 3; i++)
    arr2[i] = new int[4];
// 使用方法近似
for (int i = 0; i < 3; i++)
    for (int j = 0; j < 4; j++)
        arr[i][j] = arr2[i][j] = i & 4 + j;

它们虽然用起来差不多,但在函数传参时,如果不能正确地指定类型名是不行的。所以还是要搞懂它们的区别。

略,就是强调传值直接传指针。字符串因为有\0不用再传长度。

一路看下来没什么重要的内容。感觉一直在强调可以用指针但后面会讲更好用的引用这样。

略。

函数指针乍一看很高大上,其实声明和用法什么的是共通的。就像基本类型一样,你在函数声明的变量前加个*就是函数声明了。但因为函数有参数列表,所以要用括号括起来避免优先级的问题。如下:

double (*pf)(int);  //指向返回值为double的函数的指针
double *pf(int);    //返回值为指向double的指针的函数

此外,使用指针调用函数时,既可以使用(*pf),也可以直接使用pf当函数名。虽然两者逻辑上是冲突的,但支持两种做法的人都有,所以C++进行了折中,两种做法都是正确的。

在进行函数指针声明时,也可直接用auto+初始化的方式:

double func(int)
auto pf=func;

要查看实际的函数地址,则需要解引用,如*pf或者*(*pf)。

再上点强度,指向函数的指针的数组怎么写?

const double * (*pf[3]) (const double *, int);

如上是指向返回类型为const double *的函数的指针的数组。正如我们之前所说,pf先与[3]结合表示是数组,*表示数组内存的是指针,其他表示这是一个函数指针。如果括起来,理论上就是指向函数数组的指针了。尝试了一下,编译器将报错不允许使用函数数组。

再套一层,指向这个数组的指针怎么写?

const double * (*(*ppf)[3]) (const double *, int);

从里往外看,首先(*ppf)表示这是一个指针,把它摘出去,剩下的const double * (*[3]) (const double *, int);其实就是上面那个数组的类型了,合起来看就是指向这个类型的指针,怎么样,晕了吧。没关系,我也晕。

最后,记得用auto和typedef简化编码。例如上述的指向函数的指针的数组的指针,就可以如下定义:

const double * (*pf[3]) (const double *, int);
auto ppf=&pf;

以及使用typedef为复杂的类型起一个别名。

typedef const double * (*p_func) (const double *, int);
p_func pf=func;