c++学习

c++的一点学习笔记

1. 引用语法

引用是一种在已存在的变量或对象上创建的别名。引用提供了对现有变量的另一种名称,通过引用可以直接访问原始变量

1
type &ref_name = original_variable;

其中:

  • type 是原始变量的类型。
  • ref_name 是引用的名称。
  • original_variable 是已存在的变量。
  1. 引用必须在定义时初始化,一旦引用被初始化,它将一直引用相同的对象,不能改变引用的目标,即初始化后不可以发生改变
  2. 引用与原始变量时同一份数据,对引用的修改会直接影响原始变量,因为他们引用同一块内存
  3. 引用可以用于函数参数,引用经常用于函数参数,以便通过引用传递数据而不是通过复制
  4. 引用可以作为返回值,函数可以返回引用,使得返回值成为原始变量的别名

对于引用的理解,实际上这种语法的本质就是为变量取了一个别名,或者说为 原来有名字的内存空间 又起了一个别名,语法不难理解,实际上在编译时,这种引用的语法实际上可能会由编译器内部去转化为指针的用法,虽然实际上使用它时和指针的用法有一定区别,但这都是基于方便编程,方便修改变量(引用的设计估计就是为了方便修改变量,指针还是比较麻烦的)而设计的语法,我们只要保证使用引用的时候,一有确定的内存空间,二有合理的别名,这种对于原名,别名和内存空间的对应自然有编译器为我们代劳

引用的本质用指针的角度来看,是一个指针常量,int * const ref = &a,当内部发现是引用时,自动依据编译方式进行转换

在做一遍解释,引用在底层可能会被实现为指针,但这是编译器的实现细节,这并不影响引用在语法上的特性。在使用引用时,程序员无需关心它是否被实现为指针,只需要按照引用的语法规则使用即可。引用的主要目的是提供一种更直观、更易读的代码方式,同时避免了指针可能引发的一些问题。

2. 函数的默认参数

函数的默认参数值必须在函数声明和定义的地方同时指定。也就是说,如果你在函数声明时指定了默认参数值,那么在函数的定义时也必须再次指定默认参数值。

1
2
3
return_type function_name(type parameter1 = default_value1, type parameter2 = default_value2, ...) {
// 函数体
}

其中:

  • return_type 是函数的返回类型。
  • function_name 是函数的名称。
  • type 是参数的类型。
  • parameter1, parameter2, … 是函数的参数。
  • default_value1, default_value2, … 是参数的默认值。

如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值

注意,在C++中,函数的声明和定义不能同时带有默认参数,这是由语法规则决定的,记住是规则,不能同时带默认参数就对了

默认参数的值只能在函数的声明或者定义中的一个地方指定,而不能同时在两个地方指定。这是为了避免在不同的编译单元中可能出现的冲突问题。如果同时在声明和定义中都指定默认参数值,将导致编译错误。

这种限制确保了在不同的编译单元中对函数的一致性理解,”不同的编译单元中对函数的一致性理解” 指的是在 C++ 程序中,源代码可能被分成多个文件进行编译,每个文件被称为一个编译单元(translation unit)。编译单元是独立进行编译的基本单位,最终这些编译单元将被链接在一起形成可执行文件。

在这个背景下,对函数的一致性理解意味着在不同的编译单元中对同一个函数的声明和定义的理解是一致的。如果不同的编译单元中对同一个函数的声明和定义的默认参数值不一致,可能导致一些问题。

考虑以下情况:

1
2
3
4
5
6
7
// 文件1.cpp
void foo(int x, int y = 42); // 在声明中指定默认参数值

// 文件2.cpp
void foo(int x, int y = 24) { // 在定义中再次指定默认参数值
// 函数实现
}

在这个例子中,文件1.cpp 中的声明和文件2.cpp 中的定义对于 foo 函数的默认参数值的理解是不一致的。当这两个文件被分别编译后,如果链接在一起,可能会导致不一致的行为。这种不一致可能在程序运行时产生难以预测的结果。

为了避免这种潜在的问题,C++ 的语法规则要求默认参数值只能在声明或定义的其中一个地方指定,以确保在链接时对函数的一致性理解。

函数默认值这种用法,可以使函数调用更加灵活,不需要为每个参数都提供值,我们可以只为我们关心的参数提供值,而其他的参数就保持默认

3. 函数的占位参数

函数的占位参数是指在函数声明中省略参数名,只提供参数类型的一种方式。这种语法通常在函数声明时用于指示函数接受一定数量的参数,但在函数体内并不使用这些参数

1
2
// 函数声明中使用占位参数,省略参数名
void functionName(int a, float b, int); // 第三个参数,省略了参数名,占位用

4. 函数重载

函数重载指在同一个作用域内定义多个函数,他们有相同的名称但参数列表不同,参数列表的不同可能包括参数的类型、数量或者顺序,在调用这个函数时,编译器会根据实际传递的参数类型,数量等信息来选择匹配的函数

1
2
3
4
5
// 函数声明1
return_type function_name(type1 parameter1, type2 parameter2, ...);

// 函数声明2(重载)
return_type function_name(type3 parameter1, type4 parameter2, ...);

其中:

  • return_type 是函数的返回类型。
  • function_name 是函数的名称。
  • type1, type2, … 是参数类型

对于常量引用和普通引用,是可以作为函数重载的条件的

对于默认参数,作为函数重载的条件时,会出现二义性

5. 封装

封装是c++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法: class 类名 {访问权限: 属性/行为 }

6. c++和c#中实例化的不同

相同的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class test
{
public:
int a;
int b;
public:
void test_func()
{
printf("hello world!");
}
};


//c++
test object1;

//c#
test object1;

对于test object1这句语句,在c++中表示创建了一个对象或者实例,而在c#中仅仅声明了一个引用,并没有创建实例或者对象,需要配合使用new关键字使用

7. 访问权限

  • 公共权限 public:类内可以访问,类外也可以访问

  • 保护权限 protected:类内可以访问,类外不可以访问;儿子可以访问父亲的保护内容

  • 私有权限 private:类内可以访问,类外不可以访问;儿子不可以访问父亲的私有内容

8. 构造函数(构造器)与析构函数

对象的 初始化和清理 是两个非常中药的安全问题

c++利用了构造函数和析构函数解决上述问题,这两个哈数将会被编译器自动调用,完成对象初始化和清理工作,对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,编译器提供的构造函数和析构函数是空函数

  • 构造函数:主要作用在于创建对象时为对象的成语属性赋值,构造函数由编译器自动调用,无需手动调用
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作

8.1 构造函数

8.1.1 构造函数语法: 类名() {}
  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次

对于构造函数的访问权限问题:

  • public:如果构造函数时public的,那么在任何地方都可以创建该类的对象,这在大多数情况下是合适的,因为允许类的实例化
  • private:如果构造函数时private的,那么只有该类内部的成员函数或友元函数才能创建该类的对象,这种常用于实现单例模式或工厂模式等,确保只有受控的方式可以创建对象
  • protected:如果构造对象时protected的,那么只有该类的派生类可以访问构造函数,这在实现继承层次结构时可能会用

选择构造函数的访问权限通常是根据设计需求和类的使用场景而定

8.1.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
27
28
class Person
{
int age;

// 有参构造与无参构造
Person()
{
cout << "Person的无参构造函数调用" << endl;
}

Person(int a)
{
age = a;
cout << "Person的有参构造函数调用" << endl;
}

// 拷贝构造函数
Person (const Person &p)
{
// 将传入的人身上的所有属性,拷贝到我身上
age = p.age;
}

~Person()
{
cout << "Person的析构函数调用" << endl;
}
}

构造函数三种调用方式:

  • 括号法
  • 显式法
  • 隐式转换法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//1. 括号法
Person p; // 默认构造函数调用
Person p2(10); // 有参构造函数
Person p3(p2); // 拷贝构造函数
// 调用默认构造函数时,不要加(), 编译器会认为这是函数声明


// 2. 显式法
Person p1;
Person p2 = Person(10); // 有参构造
Person p2 = Person(p2); // 拷贝构造

// Person(10); 这条语句会创建出一个匿名对象,c++早期版本是需要调用两次构造函数来完成初始化,就是创立黎明对象调用一次,初始化变量又调用一次,后来优化就只需要调用一次,匿名对象直接变成有名对象
// 个人理解是这样,如果有不正确的希望有大佬可以帮忙指出来,谢谢~
// 要明白 初始化 和 赋值 的区别
//如果用匿名对象 初始化 另外一个同类型的对象, 匿名对象 转成有名对象
//如果用匿名对象 赋值给 另外一个同类型的对象, 匿名对象 被析构
//c++早期版本是要调用两次构造函数来完成初始化的,现在优化版本只要调用一次,提高了效率。

// 3.隐式转换法
Person p4 = 10// 相当于Person p4 = Person(10); 编译器会隐式的转成这种形式,会帮你做这种事情
Person p5 = p4;

拷贝构造函数被调用的时机:

  • 使用一个已经创建完毕的对象(不是匿名对象)来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 值方式返回局部对象

对于值方式返回局部对象,貌似是返回是回拷贝给一个匿名对象,这个时候会调用拷贝构造函数,但是根据实际情况来看,貌似现在已经不再产生临时对象,不调用拷贝构造函数,编译器做了优化,至少gcc/g++是这样的,如果加上-fno-elide-constructors的选项,关闭编译器的优化,还是会产生匿名对象,调用拷贝构造函数

弄清楚拷贝构造函数什么时候会被调用也是非常重要的,自己需要清楚这些函数的调用时机和调用次数等等,做到胸有成竹,心中有数

默认情况下,c++编译器至少会给一个类添加3个函数,默认构造函数,默认析构函数,默认拷贝构造函数

如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造

如果用户定义拷贝构造函数,c++不会再提供其他构造函数

8.1.2 深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑

浅拷贝:简单的赋值拷贝操作(编译器提供的拷贝构造函数都是浅拷贝)

深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题就是堆区内存的重复释放,当构造函数中有申请堆区内存,而使用了浅拷贝,在析构是就容易重复释放同一个块内存空间

8.2 析构函数

析构函数语法:~类名() {}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前面加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次

构造函数和析构函数不实现的时候,编译器会默认生成空实现

构造函数和析构函数都不需要写return

9. 初始化列表

在之前学习的构造函数中,主要是对类中的属性做初始化的操作,新的这个语法,初始化列表主要用途也是给类中的属性进行一个初始化的操作, 语法:

1
构造函数(): 属性1(值1), 属性2(值2)...{}

10. 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员,静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量