部分内容复习

二级指针的作用

  1. 指向指针的指针的作用:当要修改实参指针的指向的时候,形参必须使用指向指针的指针。
  2. 当修改的是实参指针指向的内容时,则形参只需是指针即可
1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char *p,int num)
{
p=(char*)malloc(sizeof(char)*num);//p是形参指向的地址
}
void main()
{
char *str=NULL;
GetMemory(str,100);//str是实参指向的地址,不能通过调用函数来申请内存
strcpy(str,"hello");
}

结构是编译能通过,却不能运行,为什么呢?

先说一下指针作为函数参数的意义:当将指针作为参数时,实参向形参传递的是地址,在函数执行过程中,既可以对该参数指针进行处理,也可以对该参数指针所指向的数据进行处理,(以上程序段来说就是可以对p或p进行处理)。*由于此时形参和实参都是指向同一个存储单元,因此当形参指针所指向的数据改变时,实参指针所指向的数据也作相应的改变,因此这时的形参可以作为输出参数使用。(str和p应同时更改!)

按照上面的说法,这个程序应该没有问题的啊,实参str和形参p指向同一个存储单元,给形参分配的内存单元应该也给实参分配了才对啊,问题就是在这里

实参和形参是指向同一个地址,它们只是指向相同,但它们自身的地址不是同时申请的,就是说p在申请内存时,相当于是把p的指向给改了,但是str的指向仍然没有改!所以尽管str调用了GetMemory,但它仍然是个空指针,所以进行strcpy是就不能运行

要使程序可以运行,只要小小的改动就行了(用指向指针的指针):

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char **p,int num)
{
  *p=(char*)malloc(sizeof(char)*num); //此时*p就变成了是形参本身的地址
}
void main()
{
  char *str=NULL;
  GetMemory(&str,100);//&str是实参的地址,所以实参和形参之间就可以直接调用
  strcpy(str,"hello");
  free(str);
}

C++对C语言的加强

namespace命名空间

C++命名空间基本常识

所谓namespace,是指标识符的各种可见范围。C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。

c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h

  1. 当使用时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
  2. 当使用的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
  3. 在程序开头使用using namespace std;,即可使命名空间std内定义的所有标识符都有效

与C的区别

  1. C中的命名空间
    • 在C语言中只有一个全局作用域
    • C语言中所有的全局标识符共享同一个作用域
    • 标识符之间可能发生冲突
  2. C++中的命名空间
    • 命名空间将全局作用域分成不同的部分
    • 不同命名空间中的标识符可以同名而不会发生冲突
    • 命名空间可以相互嵌套
    • 全局作用域也叫默认命名空间

在程序中使用命名空间

  1. 定义命名空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    namespace ns1//ns1为自定的命名空间名
    {
    const int RATE=0.08; //常量

      doublepay; //变量(可带有初始化)

      doubletax() //函数(可以是定义或声明)
    {
        return a*RATE;
      }

      namespacens2 //嵌套的命名空间
      {
        int age;
      }
    }
  2. 使用命名空间别名

    1
    2
    namespace Television //声明命名空间,名为Television
    { ... }

    可以用一个较短而易记的别名代替它。如:
    namespace TV=Television;//别名TV与原名Television等价

  3. 使用using命名空间成员名
    using nsl::Student
    在用上面的using声明后,在其后程序中出现的Student就是隐含地指nsl::Student

注意事项

在程序中如果直接使用using namespace std可以减少在变量前添加std::的繁琐编程,但是另一方面在多文件编译时可能会导致不同文件中定义的任何标识符可能与std命名空间中的同名标识符冲突,因此可以考虑使用,若干个”using命名空间成员”声明来代替“using namespace命名空间”声明,如:

1
2
3
4
5
using std::string;

using std::cout;

using std::cin;

等。

三目运算符

  1. C语言返回变量的值 C++语言是返回变量本身C语言中的三目运算符返回的是变量值,不能作为左值使用C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int main(void)
    {
    int a = 10;
    int b = 20;
    //返回一个最小数 并且给最小数赋值成30
    //三目运算符是一个表达式 ,表达式不可能做左值
    (a < b ? a : b ) = 30;//在Cpp中是可行的,而C中是错误的
    printf("a = %d, b = %d\n", a, b);
    return 0;
    }
  2. 三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用(a < b ? 1 : b )= 30;

const增强

外链C++中const关键字的使用方法总结

C++中的const修饰的,是一个真正的常量,而不是C中变量(只读)。在const修饰的常量编译期间,就已经确定下来了

1
2
3
4
//C中
const int a = 10;
int *p = (int*)&a;
*p = 11;//即可修改a中的值

C++中的const常量类似于宏定义
const int c =5; 约等于 #define 5

C++中的const常量与宏定义不同
const常量是由编译器处理的,提供类型检查和作用域检查。
宏定义由预处理器处理,单纯的文本替换

枚举

c语言中枚举本质就是整型,枚举变量可以用任意整型赋值。
而c++中枚举变量,只能用被枚举出来的元素初始化,简单来说,就是用 名字 来代表 整数值

1
2
3
4
5
6
7
8
9
10
11

enum season {SPR=1,SUM,AUT,WIN};
int main()
{
enum season s = SPR;
//s = 0; // error, 但是C语言可以通过
s = SUM;
cout << "s = " << s <<endl; //2

return 0;
}

引用

变量名,本身是一段内存的引用,即别名(alias)。引用可以看作一个已定义变量的别名

1
2
3
4
5
6
7
8
9
10
int a = 10; //c编译器分配4个字节内存, a内存空间的别名
int &b = a; //b就是a的别名
a = 11; //直接赋值
{
int *p = &a;
*p = 12;
cout << a <<endl;
}
b = 14;
cout << "a = " <<a << ", b = " << b <<endl;

  1. 引用没有定义,是一种关系型声明。声明它和原有某一变量(实体)的关系。故而类型与原类型保持一致,且不分配内存。与被引用的变量有相同的地址
  2. &符号前有数据类型时,是引用。其它皆为取地址
  3. 声明的时候必须初始化,一经声明,不可变更
  4. 可对引用,再次引用。多次引用的结果,是某一变量具有多个别名
1
2
3
4
5
int a,b;
int &r = a;
int &r = b; //错误,不可更改原有的引⽤关系
float &rr = b; //错误,引⽤类型不匹配 cout<<&a<<&r<<endl; //变量与引⽤具有相同的地址。
int &ra = r; //可对引⽤更次引用,表⽰ a 变量有两个别名,分别是 r 和 ra

引用的意义

  1. 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
  2. 引用相对于指针来说具有更好的可读性和实用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void swap(int a, int b); //无法实现两数据的交换
void swap(int *p, int *q); //开辟了两个指针空间实现交换

//使用引用
void swap(int &a, int &b){
int tmp;
tmp = a; a = b;
b = tmp;
}

int main()
{
int a = 3,b = 5;
cout<<"a = "<<a<<"b = "<<b<<endl;
swap(a,b);
cout<<"a = "<<a<<"b = "<<b<<endl;
return 0;
}

引用的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
int a = 10;
int &b = a; // 注意: 单独定义的引用时,必须初始化。
b = 11;
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%p\n", &a);
printf("&b:%p\n", &b);
return 0;
}

a:11
b:11
&a:000000000061fe14
&b:000000000061fe14
  1. 可见得Cpp编译器在定义引用时是将两个变量名指向同一个内存地址:引用在C++中的内部实现是一个常指针
    Type& name <===> Type* const name

  2. C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同

  3. 从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏。

间接赋值的三个必要条件

  1. 定义两个变量 (一个实参一个形参)
  2. 建立关联 实参取地址传给形参
  3. *p形参去间接的修改实参的值

引用在实现上,只不过是把:间接赋值成立的三个条件的后两步和二为一
当实参传给形参引用的时候,只不过是c++编译器帮我们程序员手工取了一个实参地址,传给了形参引用(常量指针)。

引用作为函数的返回值(引用当左值)

  1. 当函数返回值为引用时,若返回栈变量:不能成为其它引用的初始值(不能作为右值使用)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int getA1()
    {
    int a;
    a = 10;
    return a;
    }
    int& getA2()
    {
    int a;
    a = 10;
    return a;
    }

    //将⼀一个引⽤用赋给另⼀一个引⽤用作为初始值,由于是栈的引⽤用,内存⾮非法
    int &a3 = getA2();
  2. 当函数返回值为引用时,若返回静态变量或全局变量可以成为其他引用的初始值(可作为右值使用,也可作为左值使用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int& getA2()
    {
    static int a;
    a = 10;
    return a;
    }

    //将⼀一个引⽤用赋给另⼀一个引⽤用作为初始值,由于是静态区域,内存合法
    int &a3 = getA2();
  3. 引用作为函数返回值,如果返回值为引用可以当左值,如果返回值为普通变量不可以当左值。

    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
    //函数当左值
    //返回变量的值
    int func1()
    {
    static int a1 = 10;
    return a1;
    }
    //返回变量本⾝
    int& func2()
    {
    static int a2 = 10;
    return a2;
    }
    int main(void)
    {
    //函数当右值
    int c1 = func1();
    cout << "c1 = " << c1 <<endl;
    int c2 = func2(); //函数返回值是一个引⽤,并且当右值
    cout << "c2 = " << c2 <<endl;
    //函数当左值
    //func1() = 100; //error
    func2() = 100; //函数返回值是一个引用,并且当左值
    c2 = func2();
    cout << "c2 = " << c2 <<endl;
    return 0;
    }

指针引用

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
struct Teacher
{
char name[64];
int age ;
};

// 在被调用函数 获取资源
int getTeacher(Teacher **p)
{
Teacher *tmp = NULL;
tmp = (Teacher*)malloc(sizeof(Teacher));
tmp->age = 33;
// p是实参的地址 *实参的地址 去间接的修改实参的值
*p = tmp;
return 0;
}

// 指针的引用 做函数参数
int getTeacher2(Teacher* &myp)
{
// 给myp赋值 相当于给main函数中的pT1赋值
myp = (Teacher *)malloc(sizeof(Teacher));
if (myp == NULL)
return -1;
myp->age = 36;
return 0;
}
void FreeTeacher(Teacher *pT1)
{
if (pT1 == NULL)
return;

free(pT1);
}

int main(void)
{
Teacher *pT1 = NULL;
// 1 c语⾔言中的二级指针
getTeacher(&pT1);
cout << "age:" << pT1 -> age << endl;
FreeTeacher(pT1);
// 2 c++中的引用(指针的引用)
// 引用的本质间接赋值后2个条件,让c++编译器帮我们程序员做了。
getTeacher2(pT1);
cout << "age:" << pT1 -> age << endl;
FreeTeacher(pT1);
return 0;
}

const 引用

const引用,它可以防止对象的值被随意修改。具有一些特性。

  1. const对象的引用必须是const的,将普通引用绑定到const对象是不合法的

    1
    2
    const int a = 1;
    int &b = a;//这是不合法的
  2. const引用可使用相关类型的对象(常量,非同类型的变量或表达式)初始化。这个是const引用与普通引用最大的区别。

    1
    2
    3
    4
    //以下语句是合法的
    const int &a = 2;
    double x =3.1;
    const int &b =a;
1
2
3
4
5
6
7
8
//普通引用
int a = 10;
int &b = a;

//常引用
int x = 20;
const int &y = x; //常引用是限制变量为只读不能通过y去修改x了
//y = 21; //error

const 引用的原理

const引用是指向const对象的引用:
通过ref2对ival赋值会导致修改const对象的值,为防止这样的修改,需要规定将普通的引用绑定到const对象是不合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
const int ival =1024;
const int &refVal = ival; //两者均为const对象
int &ref2 = ival; //error!不能使用非const引用指向const变量
}

{
double val =3.14;
// double *ref = &val;
// const double& ref = val;
const int& ref = val;
// int& refi = vali; //error
double & ref2 = val;
cout << ref << " " << ref2 <<endl;
val = 5.22;
cout << ref << " " << ref2 <<endl;
}


同样的初始化对于非const引用却是不合法的,而且会导致编译时错误,观察将引用绑定到不同的类型时所发生的事情,最容易理解上述行为。对于以下代码

1
2
3
4
5
6
double dval = 3.14;
const int &ri = dval;

//编译器会将这些代码转换为以下形式
int temp = dval;
const int &ri = temp;

可以发现对于将引用绑定到不同类型时,编译器会创建一个int型的暂时变量存储dval,然后将ri绑定到temp上

(Tips:引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,引用实际上指向该对象,但用户不能访问它。)

  • 结论
    1. const int & e 相当于 const int * const e
    2. 普通引用 相当于 int *const e
    3. 当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
    4. 使用字面量对const引用初始化后,将生成一个只读变量

inline内联函数

类似于C语言中宏函数,但是C中的宏函数处理发生在预处理阶段,没有语法检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline void func(int a)
{
a = 20;
cout << a <<endl;
}
int main(void)
{
func(10);
/*
//编译器将内联函数的函数体直接展开
{
a = 20;
cout << a <<endl;
}
*/
return 0;
}
  1. 特点:

    1. 内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。
    2. C++编译器直接将函数体插入在函数调用的地方。
    3. 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。
    4. 内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)。
    5. 内联函数由编译器处理,直接将编译后的函数体插入调用的地方,宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程。
    6. C++的内联编译应该不包含循环、过多的条件判断、不宜实现太过复杂的的功能。
    7. 内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
  2. 总结:

    • 本质:以牺牲代码段空间为代价,提高程序的运行时间的效率。
    • 适用场景:函数体很“”,且被“频繁”调用

函数默认参数

单个默认参数

对于多次调用一函数同一实参时,C++给出了更简单的处理办法。给形参以默认值,这样就不用从实参那里取值了。

1
2
3
4
5
//1 若你填写参数,使⽤用你填写的,不填写默认
void myPrint(int x = 3)
{
cout<<"x: "<<x<< endl;
}

多个默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//2 在默认参数规则 ,如果默认参数出现,那么右边的都必须有默认参数
float volume(float length, float weight = 4,float high = 5)
{
return length*weight*high;
}
int main()
{
float v = volume(10);
float v1 = volume(10,20);
float v2 = volume(10,20,30);
cout<<v<<endl;
cout<<v1<<endl;
cout<<v2<<endl;
return 0;
}

占位参数

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
/*
函数占位参数
占位参数只有参数类型声明,⽽而没有参数名声明
⼀一般情况下,在函数体内部⽆无法使⽤用占位参数
*/
int func(int a, int b, int)
{
return a + b;
}

int main()
{
func(1,2); //error,必须把最后一个占位参数补上

func(1,2,3); //right
}

/*
可以将占位参数与默认参数结合起来使用
意义:为以后程序的扩展留下线索兼容C语⾔言程序中可能出现的不规范写法
*/

//C++可以声明占位符参数,占位符参数⼀一般⽤用于程序扩展和对C代码的兼容
int func2(int a, int b, int = 0)
{
return a + b;
}
int main()
{
//如果默认参数和占位参数在一起,都能调用起来
func2(1, 2);
func2(1, 2, 3);
return 0;
}

函数重载

规则(类似Java)

  1. 函数名相同。
  2. 参数个数不同,参数的类型不同,参数顺序不同,均可构成重载。
  3. 返回值类型不同则不可以构成重载。
  4. 一个函数,不能既作重载,又作默认参数的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int func(int a, int b, int c = 0)
{
return a * b * c;
}
int func(int a, int b)
{
return a + b;
}
int func(int a)
{
return a;
}
int main()
{
int c = 0;
c = func(1, 2); //error. 存在二义性,调用失败,编译不能通过
printf("c = %d\n", c);
return 0;
}

C++基本

头文件保护

引出:重复定义问题

考虑以下示例:
File_1.h头文件包含以下内容

1
2
3
4
int getNumber()
{
return 4;
}

File_2.h

1
#include "File_1.h"

main.cpp

1
2
3
4
5
6
7
#include "File_1.h"
#include "File_2.h"

int main()
{
return 0;
}

这个看似正常的程序无法编译!下面是所发生的事情。首先,main.cpp包含File_1.h,它将函数getNumber的定义复制到main.cpp中。然后,main.cpp包含File_2.h,间接包含File_1.h。这会将File_1.h的内容(包括函数getNumber的定义)复制到File_2.h中,然后将其复制到main.cpp中。

因此,在解析所有#include之后,main.cpp最终如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
int getNumber() //from File_1.h
{
return 4;
}

int getNumber() //from File_2.h
{
return 4;
}
int main()
{
return 0;
}

或者是下面这个例子
假设你有两个类:

1
2
3
// A.h
#include "B.h"
class A { B b; };

1
2
3
// B.h
#include "A.h"
class B { A a; };

互相 include。

如果头文件里面 没有 include guard
编译器在展开 #include 的时候,会无限互相展开 → 死循环。

或者即使没有互相 include
一个头文件被 include 多次,类会重复定义:
error: redefinition of class X

头文件保护

我们可以通过一种称为头文件保护(include guard 也称为包含保护)的机制来避免上述问题。头文件保护是采用以下形式的条件编译指令:

1
2
3
4
5
6
7
8
//但根据惯例,它被设置为头文件的完整文件名,以大写字母键入,使用下划线表示空格或标点
//例如:square.h将被表示为:SQUARE_H
#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// 这里放置你的声明

#endif

基本调试策略

  1. 注释代码
  2. 验证代码流程
    在需要验证的代码前添加调试输出语句。
    打印用于调试的信息时,请使用std::cerr而不是std:∶cout。因为,std::cout可能有缓冲区而std::cerr没有缓冲,这意味着您发送给它的任何内容都将立即输出
  3. 条件化调试代码
    在引入调试代码后,完成调试后,您要么需要删除它们,要么将它们注释掉。如果以后需要它们,则必须重新添加它们,或取消注释它们。非常麻烦且容易扰乱正常的代码功能。
    使用预处理指令,可以很容易的在整个程序中启用或禁用调试语句
    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
    #include <iostream>

    #define ENABLE_DEBUG // 注释掉这一行,可以禁用掉调试输出
    //如果这是一个多文件程序,#define ENABLE_DEBUG 将放在头文件中,被所有用到的地方引用,方便我们可以在单个位置注释/取消注释#define,并将其传播到所有代码文件。

    int getUserInput()
    {
    #ifdef ENABLE_DEBUG
    std::cerr << "getUserInput() called\n";
    #endif
    std::cout << "Enter a number: ";
    int x{};
    std::cin >> x;
    return x;
    }

    int main()
    {
    #ifdef ENABLE_DEBUG
    std::cerr << "main() called\n";
    #endif
    int x{ getUserInput() };
    std::cout << "You entered: " << x << '\n';

    return 0;
    }

  4. 使用日志记录器(Logger)
    现代常用的调试和错误记录方式,具体内容可以在网络上学习

面向对象

类和对象的关系

类是模板,对象是根据这个模板创建出来的实例

类和结构体的关系

使用结构体完成一段功能程序:

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
struct Node{
// 成员变量
string name;
int age;

// 成员函数
void setValue(string name1,int age1){
name = name1;
age = age1;
}
void show(){
cout<<name<<" "<<age;
}

}node;

int main()
{
string name;
int age;
cin>>name>>age;
node.setValue(name,age);
node.show();
return 0;
}

将这段结构体改为类的形式:

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
class CNode{
public:
// 成员变量
string name;
int age;

// 成员函数
void setValue(string name1,int age1);
void show(){
cout<<name<<" "<<age;
}
};
// 如果函数实现太长可以放在类定义外,类中只放函数声明即可
void CNode::setValue(string name1,int age1){
name = name1;
age = age1;
}

int main()
{
CNode node; //定义一个CNode类
string name;
int age;
cin>>name>>age;
node.setValue(name,age);
node.show();
return 0;
}

简单来看C++中的类和结构体其实含义是相似的只是在表现形式上有所差别,但是其使用的思想是不太相同的。

类的权限

访问属性 属性 对象内部 对象外部
public 公有 可访问 可访问
protected 保护 可访问 不可访问
private 私有 可访问 不可访问

struct中所有行为和属性都是public 的(默认)。
C++中的class可以指定行为和属性的访问方式,默认是private。

对象的构造和析构

这就不得不提中文翻译的不便之处了,从英文意思来看很简单,
构造函数就是对对象进行基本的初始化
析构函数就是在对象销毁前完成一些清理工作

构造函数 Constructor

基本语法

作用:

  • 当对象被创建时自动调用,用于初始化对象的成员变量或执行其他初始化操作。
    特点:
  1. 函数名必须和类名相同。
  2. 没有返回值(连 void 都不能写)。
  3. 可以有多个构造函数(函数重载),实现不同的初始化方式。
  4. 可以有默认参数,也可以写成 默认构造函数(无参数)。
  5. 访问权限必须是public
  6. 创建对象时只会自动调用一次,不能手动调用
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
class CNode{
private:
// 成员变量
string name;
int age;

public:
// 构造函数
CNode(){
name = "";
age = -100;
}
// @overload constructor
CNode(string name1){
name = name1;
age = 0;
}
// 成员函数
void setValue(string name1,int age1);
void show();
};
int main()
{
CNode node; //定义一个CNode类,调用默认构造
CNode node("张三"); //调用重载构造
...
}

注意:对构造函数进行重载的适合也遵从重载的要求:默认参数可能影响重载

1
2
void f(int x, int y = 0);
void f(int x); // ❌ 可能会产生二义性

初始化列表

基本语法:
类名(形参列表):成员1(值1),成员2(值2)... {......}

示例:

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
class CNode{
private:
// 成员变量
string name;
int age;
public:
// 构造函数
CNode(){
name = "";
age = -100;
}
// @overload constructor
CNode(string name1):name(name1),age(0){
//使用了初始化列表
}
// 成员函数
void setValue(string name1,int age1);
void show();
};
int main()
{
CNode node; //定义一个CNode类,调用默认构造
CNode node("张三"); //调用重载构造
...
}

注意事项:

  1. 从表现上来看使用初始化列表和直接对变量进行赋值没有区别。但是如果需要初始化的成员是类,使用初始化列表调用的是成员类的拷贝构造函数,而赋值则是先创建成员类的对象(调用成员类的普通构造函数),然后再赋值

  2. 如果成员是常量(const)和引用,必须使用初始化列表,因为常量和引用只能在定义的时候初始化

析构函数 Destructor

作用:

  • 当对象生命周期结束时自动调用,用于释放资源或执行清理操作(如释放内存、关闭文件等)。

特点:

  1. 函数名 类名前加 ~(波浪号)。
  2. 没有返回值,不能有参数,也不能重载。
  3. 对象销毁时自动调用,无需手动调用。
  4. 创建对象时只会自动调用一次,可以手动调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 节选
public:
// 构造函数
CNode(){
name.clear();
age = 0;
}
// @overload constructor
CNode(string name1){
name = name1;
age = 0;
}
// 析构函数
~CNode(){
cout<<"调用析构函数";
}

拷贝构造函数

用一个已存在对象创建一个新对象时调用的构造函数
也就是说新对象,不由构造函数来构造,而是由拷贝构造函数来完成。拷贝构造函数的格式是固定的。

语法:

1
2
3
4
5
6
7
class 类名
{
类名(const 类名 &形参名)
{
拷贝构造体
}
}

示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
public:
string name;
Person(){
name.clear();
cout << "构造函数调用" << endl;
}
Person(const Person& other) // 拷贝构造函数
{
this->name = "拷贝"+other.name;
cout << "拷贝构造函数调用" << endl;
}
~Person(){
//析构函数
}
};

int main(){
Person p;
p.name = "张三";
Person p2 = p;
cout<<p.name<<" "<<p2.name;
return 0;
}

  • 拷贝构造函数可以重载,也可以有默认参数
  • 如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数

浅拷贝和深拷贝

类型 拷贝内容 优点 缺点
浅拷贝 拷贝指针的值(只复制地址) 会导致重复释放、内存问题
深拷贝 拷贝指针指向的数据,重新开辟空间 安全、不互相影响 多一次开销
  • 什么时候必须深拷贝?
    类里有指针成员(尤其是 new 出来的资源)
    必须写深拷贝,否则一定出 bug

简单来说,当类类中有指针成员的时候,使用浅拷贝会在拷贝赋值的时候将两个指针指向一片内存,当类A生命周期结束会释放这片内存。
如果之后拷贝的类也结束了这个类的指针就成了野指针,再去尝试释放当初指向的内存就会导致错误崩溃。
而C++中默认的拷贝构造函数使用的都是浅拷贝,所以当类中需要用到指针时,一定要手动写深拷贝的拷贝构造函数。

看下面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
public:
int age;
int* score; //指针成员

Person(int a, int s){
age = a;
score = new int(s);
}
};

//假设我们这样操作
Person p1(18, 90);
Person p2 = p1; // 拷贝构造,默认浅拷贝


// 深拷贝的拷贝构造函数
Person(const Person& p){
age = p.age;
score = new int(*p.score); // 深拷贝
}

注意事项

  1. 在构造函数后面加括号和参数不是调用构造函数,而是创建匿名对象
    看下面这个例子:
    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
    class CNode{
    private:
    // 成员变量
    string name;
    int age;

    public:
    // 构造函数
    CNode(){
    name = "";
    age = -100;
    }
    // @overload constructor
    CNode(string name1){
    CNode(); //错误点:调用默认构造函数初始化,再对参数进行初始化
    name = name1;

    }
    // 成员函数
    void setValue(string name1,int age1);
    void show();
    };
    int main()
    {
    CNode node("张三"); //调用重载构造
    ...
    }

如果使用上面这个构造方式会导致非常严重的问题,在传参的重载构造函数中我们调用了CNode(),如果在类中需要初始化的内容很多这样写,似乎可以减少代码量。但是事实上这并不是调用构造函数,而是创建了匿名对象,并没有对对象进行初始化。如果在对象中使用了指针就会产生野指针,甚至导致程序崩溃。

  1. 下面两行代码有本质区别
    1
    2
    3
    4
    5
    // 方法1
    CNode node = CNode("张三"); //创建对象
    // 方法2
    CNode node; //创建对象
    node = CNode("张三"); //创建匿名对象,然后给现有对象赋值

上面代码的方法2 其实调用了两次构造函数和析构函数

创建对象的方式

栈上创建

栈上创建对象(直接创建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
public:
string name;
Person(string n) : name(n) { cout << "构造函数: " << name << endl; }

/*
等价于
:name(n)是初始化列表写法
Person(string n){
name = n;
cout << "构造函数: " << name << endl;
}
*/
~Person() { cout << "析构函数: " << name << " 被调用" << endl; }
};

int main() {
Person p("张三"); // 栈上创建
cout << p.name << endl;
} // p 超出作用域,析构函数自动调用

堆上创建

堆上创建对象(使用 new 和 delete)

1
2
3
4
5
6
7
8
9
10
11
12

#include <iostream>
using namespace std;
// 类定义省略

int main() {
// new返回创建对象的地址,要使用指针
Person* p = new Person("李四"); // 堆上创建,调用构造函数
cout << p->name << endl;

delete p; // 手动释放内存,调用析构函数
}

两种方式的对比

对比项 栈对象 堆对象
内存位置
生命周期 随作用域结束自动销毁 程序员控制,通过 delete
内存管理 自动管理 需手动管理
访问方式 . ->(指针访问)
创建速度 慢一些
安全性 容易忘记释放,可能泄漏
使用场景 对象数量小、生命周期固定 对象数量大或生命周期不确定
  • 绝大多数情况下,直接在栈上创建对象更好

    • 简单、安全,效率高
    • 不需要手动管理内存
  • 只有在以下情况才建议用 new:

    • 对象数量非常多,栈空间不够
    • 对象需要跨作用域或动态控制
    • 对象需要在运行时动态创建和销毁
  • 现代 C++ 更推荐使用 智能指针,避免因为忘记delete而导致的内存泄漏

const修饰成员函数

在成员函数中后加 const 关键字修饰即可
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Node{
private:
string name;
int age;

public:
Node(){
name = "张三"; age = 0;
}
void show() const;
};
void Node::show() const{
cout<<name<<" "<<age<<endl;
}

  • const修饰的成员函数只能调用const修饰的成员函数,非cosnt修饰的成员函数则不受限制
  • const修饰的对象只能调用const修饰的成员函数,非cosnt修饰的对象则不受限制

this指针

this 指针是 C++ 类中一个 隐式存在的指针
每一个成员函数里都带着它。
它指向当前对象本身。

使用 return 返回时使用 *this 返回当前对象

类的静态成员

作用:静态成员变量和静态成员函数可以在多个对象之间共享数据,比全局变量更加安全

本质:将类的成员声明为静态的,就可以把它和类的对象独立开来,或者说静态成员不属于对象而是只属于类的,整个程序中只储存一份

使用注意:

  • 静态成员变量不会再创建对象的适合初始化,需要在程序的全局区用代码清晰的初始化
  • 静态成员使用类名加::即可访问,不需要创建对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Node{
    static int age;
    };

    int Node::age; //初始化

    int main(){
    cout<<Node::age;
    ...
    }

总结:
| 类型 | 属于 | 是否有 this | 所有对象是否共享 |
| ——— | — | ———— | ———— |
| 普通成员变量 | 对象 | 有 | 否 |
| 静态成员变量 | 类 | 无 | 是 |
| 普通成员函数 | 对象 | 有 | — |
| 静态成员函数 | 类 | 无 | — |

友元

关键字: friend
产生原因:
如果要访问类的私有成员变量,可以通过调用类的public 成员函数,但是类的private 成员函数则无法访问。友元则提供了一种访问类的private 成员的方法。

友元的定义可以放在类中的任意位置

友元有三种:

  • 友元全局函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Person{
    friend void showAge(Person& p); // 声明 showAge 是 Person 的友元
    private:
    int age;
    public:
    Person(){}
    Person(int a):age(a){}

    };

    void showAge(Person& p){
    cout << p.age << endl; // 可以访问 private
    }

  • 友元类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class A{
    friend class B; // 整个 B 类都可以访问 A 的 private
    private:
    int x;
    public:
    A():x(10){}

    };

    class B{
    public:
    void f(A& a){
    cout << a.x << endl; // 可以访问 A::x
    }
    };

注意事项:

  • 友元关系不能被继承
  • 友元关系是单向的,不具备交换性
  • 友元成员函数
    假设有两个类 A 和 B,我们希望 B 的某个成员函数能访问 A 的私有成员,而不是整个类 B 都可以访问:
    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
    class B; // 前向声明

    class A {
    private:
    int x;
    public:
    A(int val) : x(val) {}

    // 声明 B 类的成员函数 f 为友元
    friend void B::f(A& a);
    };

    class B {
    public:
    void f(A& a) {
    cout << "访问 A 的私有成员 x = " << a.x << endl;
    }
    };

    int main() {
    A a(100);
    B b;
    b.f(a); // 可以访问 A::x
    }

说明:

  1. 需要前向声明:由于 A 里要声明 B::f 为友元,必须提前告诉编译器 B 是一个类
  2. friend 声明:写在类 A 内,声明 B::f 是 A 的友元成员函数

类的自动类型转换

概念:
C++ 支持对象在某些情况下自动转换成其他类型,或者其他类型转换成对象

C++基础类型 → 类对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
int x;
public:
A(){}
A(int val) : x(val) {} // 单参数构造函数
void show() { cout << x << endl; }
};

int main() {
A a = 10; // int 自动转换为 A 对象,调用 A(10)
a.show(); // 输出 10
}

int main(){
A a1(8); // 常规写法
A a1 = A(8); //显式转换
A a1 = 8 //隐式转换

//先创建对象再赋值
A a1;
a1 = 8;
}

说明:

  1. 单参数构造函数可以被用作 类型转换构造函数

禁止自动类型转换:
如果不想允许隐式转换(仍可以显示转换),可以在单参数构造函数前加 explicit

1
2
3
4
5
6
7
8
9
10
11
class A {
int x;
public:
explicit A(int val) : x(val) {} //禁用隐式转换
};

int main() {
// A a = 10; // 错误!必须显式调用
A a(10); // 正确
}

类 → 其他类型

通过在类中定义 operator Type()函数实现。

注意:

  • 在实际开发中使用operator进行类型转换不如使用自己手动编写的类型转换函数,因为隐式的转换有时候可能会导致错误和二义性,反而不利于代码维护和排查错误

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
int x;
string y;
double z;
public:
A(int val) : x(val) {}

// 类 A → int 的转换
operator int() { return x; }
// 类 A → string 的转换
operator string() { return y; }
// 类 A → double 的转换
operator double() {return z; }
};

int main() {
A a;
int n = a; // 调用 operator int() 自动转换
string y = string(A);
double z = double(A);
}