前言
- 这一篇章算是C++栏目的语法的第一篇章,主要是为类和对象打下基础,介绍一些C++基本的语法结构
- 这里提醒一下,要是看不懂第一步创建文件的,其实更建议先学习一下C语言,因为C++的学习往往已经有点语法和数据结构的基础了
- 那么就更建议从C语言开始学习,从下而上的学习,关于C语言篇章的博客,还是有些不足的,以后我会重新编写一下C语言篇章的博客
- C语言的系统性学习_Jason_from_China的博客-CSDN博客
C++创建文件
C++创建的文件在Visual Studio 2022里面是.cpp,而C语言是.c
这里需要注意的是,虽然C++兼容C语言,但是C++的语法结构比C语言更加严谨
C++ namespace(域)
namespace的价值
避免命名冲突:在大型项目或使用多个库的情况下,不同部分可能会定义相同名称的实体(如变量、函数、类等)。使用命名空间可以有效地避免这些命名冲突。【这一点的尤其关键的,在C语言里面,是没有域这样的区分之说的,所以会导致命名的冲突,尤其的当命名应该新变量的时候,如果引入新的库,会导致命名错误,就需要大量的修改命名,这会很麻烦。但是在C++里面,我们只需要在外层封装一层就可以了】
//这个C语言的代码就是有问题的,因为引入了新的库,从而导致需要更改命名, //这里还是比较少的代码,如果代码数量比较多,那么就会存在大量重复的代码的工作 #include <stdio.h> #include <stdio.h> #include <stdlib.h> int rand = 10; int main() { // 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数” printf("%d\n", rand); return 0; }
组织代码逻辑:命名空间可以将逻辑相关的代码组织在一起,使得代码结构更加清晰,便于理解和维护。
提供上下文:命名空间提供了一种上下文,使得程序员可以快速了解某个实体属于哪个模块或库。
支持大型项目开发:在大型项目中,可能需要包含多个库和子系统,命名空间的使用可以确保这些不同的部分在全局作用域中不会相互干扰。
促进代码重用:命名空间允许程序员在不同的上下文中重用相同的名称,而不必担心名称冲突,从而提高了代码的重用性。
简化代码编写:使用命名空间后,程序员可以在不同的命名空间中定义具有相同名称的变量或函数,简化了代码编写。
支持模块化开发:命名空间支持模块化开发,每个模块可以有自己的命名空间,从而实现代码的解耦。
提高编译效率:编译器在查找未限定名称时,会先在局部作用域查找,然后是类作用域,接着是命名空间作用域,最后是全局作用域。这种分层次的查找顺序可以提高编译效率。
便于代码维护:当需要修改或更新某个模块时,只需要关注该模块的命名空间,减少了对其他模块的影响。
支持语言的扩展性:随着 C++ 语言的发展,新的库和特性不断加入,命名空间的使用确保了新旧代码可以和谐共存,不会因新特性的加入而导致现有代码出现问题。
namespace定义
- 定义命名空间,需要使用到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
- namespace只能定义在全局,当然他还可以嵌套定义。
- 项目工程中多文件中定义的同名namespace会认为是⼀个namespace,不会冲突。
- C++标准库都放在⼀个叫std(standard)的命名空间中
- namespace是C++基础关键字,不需要什么头文件
namespace单层的书写和调用
这里Js是命名空间的名字,一般在开发里面用项目的名字为命名空间的名字
这里我是随意起了一个名字
域里面也是可以书写函数和结构体等等的
//namespace只能定义在全局,当然他还可以嵌套定义。 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> namespace Js { int c = 10; int ADD(int a, int b) { return a + b; } } int main() { printf("%d", Js::c); return 0; }
#include<stdio.h>
:这是另一个预处理指令,用于包含标准输入输出库的头文件。这个库提供了printf
函数,它在main
函数中被用来输出。
namespace Js { ... }
:这里定义了一个名为Js
的命名空间。命名空间是一种封装机制,用于组织代码,防止命名冲突。在这个命名空间中:
int c = 10;
定义了一个名为c
的整型变量,并初始化为10
。int ADD(int a, int b) { ... }
定义了一个名为ADD
的函数,它接受两个整型参数a
和b
,并返回它们的和。
int main() { ... }
:这是程序的主函数,程序从这里开始执行。
printf("%d", Js::c);
在这里,Js::c
表示对命名空间Js
中的成员变量c
的引用。printf
函数使用Js::c
作为参数,输出c
的值,即10
。总结来说,
Js::c
是对Js
命名空间中定义的静态成员变量c
的引用。在main
函数中,通过Js::c
访问该变量,并使用printf
函数将其值输出到控制台。这里可以看到结果,进入到命名空间里面访问c,c我们初始化给的数值是10,所以访问的时候就是10
::域作用限定符
域作用限定符这里主要的作用就是访问域里面的变量或者函数
下面的如果看不懂没关系,直到域作用限定符在这里面是干什么用的就可以
访问全局变量或函数:当局部作用域(如函数内部)中存在与全局作用域中同名的变量或函数时,可以使用
::
来指定访问全局作用域中的实体。//C++的输出方式 int globalVar = 10; // 全局变量 void function() { int localVar = 20; // 局部变量 std::cout << ::globalVar << std::endl; // 访问全局变量 }
//C语言的输出方式 int a = 110; int main() { printf("全局变量的a=110,局部变量的a=11\n\n\n"); int a = 11; printf("不加域作用限定符:%d\n\n\n", a); printf("加上域作用限定符:%d\n\n\n", ::a); }
访问命名空间中的成员:在包含多个命名空间的情况下,
::
用于指定访问特定命名空间中的成员。namespace A { int value = 5; } namespace B { int value = 10; } int main() { std::cout << ::A::value << std::endl; // 输出 A 命名空间中的 value std::cout << ::B::value << std::endl; // 输出 B 命名空间中的 value }
访问类的静态成员:在类的作用域内部访问静态成员时,需要使用
::
来指定。class MyClass { public: static int staticVar; }; int MyClass::staticVar = 0; // 定义静态成员 void function() { MyClass::staticVar = 1; // 在类外访问静态成员 }
解决作用域冲突:当嵌套作用域中存在同名实体时,
::
用于解决这些冲突,明确指定要访问的实体。int outerVar = 1; void outerFunction() { int innerVar = 2; std::cout << ::outerVar << std::endl; // 访问外部变量 }
访问枚举类型的值:在 C++11 之前,枚举类型没有强类型支持,使用
::
可以访问枚举中的值。enum Color { RED, GREEN, BLUE }; int main() { std::cout << ::RED << std::endl; // 访问 RED 枚举值 }
namespace嵌套的书写和调用
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> namespace Js { int c = 10; int ADD(int a, int b) { return a + b; } namespace js1 { int d = 30; int f = 20; int del(int d, int f) { return d - f; } } namespace js2 { int d = 30; int f = 20; int ADD(int d, int f) { return d + f; } } } int main() { printf("namespace单层调用测试:%d\n", Js::c); int ret = Js::js1::del(30, 10); printf("namespace多层调用测试:%d", ret); return 0; }
最终效果
- 在调用的时候,我们可以利用域作用限定符
- 进入到我们需要的域里面调用函数,并且选择接收。
- 我们既可以初始化,也可以选择传参,和C语言里面函数的使用是一样的。只是多了一层的封装。
namespace多文件定义-同变量名
在 C++ 中,命名空间(
namespace
)是一种封装机制,用于组织代码并防止名称冲突。当你在多个文件中定义同名的命名空间时,C++ 编译器会将这些命名空间视为同一个命名空间的扩展。这意味着不同文件中定义的同名命名空间的成员实际上是共享的,它们属于同一个逻辑上的命名空间。这种特性非常有用,因为它允许你将大型项目分解为多个模块或文件,每个模块都有自己的命名空间,同时又能够保持命名空间的一致性和统一性。
File1.cpp:
namespace MyProject { void function1() { std::cout << "Function 1" << std::endl; } }
File2.cpp:
namespace MyProject { void function2() { std::cout << "Function 2" << std::endl; } }
在这两个文件中,我们都定义了名为
MyProject
的命名空间,并在其中分别定义了function1
和function2
。当我们编译和链接这两个文件时,MyProject
命名空间将被视为同一个命名空间,function1
和function2
都将是MyProject
命名空间的成员。你可以在
main
函数或其他任何地方这样调用它们int main() { MyProject::function1(); // 调用 File1.cpp 中定义的 function1 MyProject::function2(); // 调用 File2.cpp 中定义的 function2 return 0; }
等同于一个域:
多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样
namespace MyProject { void function1() { std::cout << "Function 1" << std::endl; } void function2() { std::cout << "Function 2" << std::endl; } }
当然,如果你两个域封装的函数名字也一样,但是作用不一样,那么我们只需要再封装一下就可以。
namespace实际项目举例
实际使用
如图
当为同一个命名空间下面,我们还可以嵌套定义。
防止同一个项目组命名冲突
这样就增加了团队合作的协作性,而且这里有一个关键,是C语言没有的,就是不同域之间的命名冲突是不会导致报错的,因为域的不同,但是如果域相同命名一样,还是会导致冲突,解决方式就只需要再封装一下就可以,所以这里我们就可以发现C++比C语言优势多了很多了
namespace命名空间的展开
命名空间的展开顾名思义就是不需要继续进入到域里面了,直接就可以访问
命名空间展开的弊端和使用规则:
展开命名空间(风险很大,平时写的小算法,写的小项目,没有那麽多的冲突的时候,是可以直接展开,命名空间)
展开指定的命名空间:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> namespace Js { int c = 10; int ADD(int a, int b) { return a + b; } namespace js1 { int d = 30; int f = 20; int del(int d, int f) { return d - f; } } namespace js2 { int d = 30; int f = 20; int ADD(int d, int f) { return d + f; } } } int a = 110; //int main() //{ // printf("全局变量的a=110,局部变量的a=11\n\n\n"); // int a = 11; // printf("不加域作用限定符:%d\n\n\n", a); // printf("加上域作用限定符:%d\n\n\n", ::a); // // printf("namespace单层调用测试:%d\n\n\n", Js::c); // // int ret = Js::js1::del(30, 10); // printf("namespace多层调用测试:%d\n\n", ret); // // //展开命名空间 // // return 0; //} using namespace Js; int main() { int c = 0; printf("展开命名空间,访问局部空间有的变量:%d\n\n\n", c); printf("展开命名空间,访问局部空间有的变量:%d", ::c); return 0; }
- 这里可以发现,::域作用限定符存在依旧是会首先访问全局变量,不存在会首先访问局部变量
- 这里已经展开Js这个域,所以c就暴露在全局变量了
展开嵌套的命名空间:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> namespace Js { int c = 10; int ADD(int a, int b) { return a + b; } namespace js1 { int d = 30; int f = 20; int del(int d, int f) { return d - f; } } namespace js2 { int d = 30; int f = 20; int ADD(int d, int f) { return d + f; } } } int a = 110; //int main() //{ // printf("全局变量的a=110,局部变量的a=11\n\n\n"); // int a = 11; // printf("不加域作用限定符:%d\n\n\n", a); // printf("加上域作用限定符:%d\n\n\n", ::a); // // printf("namespace单层调用测试:%d\n\n\n", Js::c); // // int ret = Js::js1::del(30, 10); // printf("namespace多层调用测试:%d\n\n", ret); // // //展开命名空间 // // return 0; //} using namespace Js; using namespace Js::js2; int main() { int c = 0; printf("展开外层命名空间,访问局部空间有的变量:%d\n\n\n", c); printf("展开外层命名空间,访问局部空间有的变量:%d\n\n\n", ::c); int d = 00; printf("展开嵌套内层命名空间,访问局部空间有的变量:%d\n\n\n", d); printf("展开嵌套内层命名空间,访问局部空间有的变量:%d\n\n\n", ::d); return 0; }
C++的流提取(>>)(输入) & 流插入(<<)(输出)
什么是输入和输出流
流提取(<<)(输入)
- 理解:我们可以理解为,输入到io流里面,比如是cin,然后从输入流中读取数据
流插入(<<)(输出)
- 理解:我们可以理解把数据插入到io流里面,比如是cout,然后通过这个流将数据输出到屏幕或其他输出设备。
C++输入流和输出流的优点
类型安全:C++的I/O流机制可以自动识别变量类型(不需要写整形,字节这样的类型),从而提供类型安全。这意味着编译器会在编译时检查类型不匹配错误,而C语言中的
printf
和scanf
系列函数在运行时才检查类型,容易导致运行时错误。可读性:C++的流式I/O操作符
<<
和>>
提供了直观的语法,使得代码易于阅读和编写。与C语言的格式化字符串相比,C++的I/O操作符更易于理解。易于使用:C++的I/O机制使用起来更简单,不需要记住复杂的格式化字符串。对于基本数据类型,直接使用
<<
或>>
即可完成输入输出。扩展性:C++的I/O流库可以很容易地扩展以支持自定义类型的输入输出(自定义类型,使用这个比较方便)。通过运算符重载,可以为自定义类型定义
<<
和>>
运算符,从而实现自定义类型的输入输出。面向对象:C++的I/O流库是基于面向对象设计的,
std::istream
和std::ostream
是从这些类派生的。这种设计使得I/O操作可以与面向对象的程序设计方法无缝集成。本地化支持:C++的I/O流库支持本地化
异常处理:C++的I/O操作可以抛出异常,这使得错误处理更加灵活和强大。而C语言的I/O操作通常只是返回一个错误码。
缓冲管理:C++的I/O流库自动管理缓冲区,开发者不需要手动刷新缓冲区。例如,
std::endl
不仅插入一个换行符,还刷新了输出缓冲区,而C语言中的\n
仅插入换行符。注意事项
- 一般情况下我们可以
using namespace std
,实际项目开发中不建议using namespace std,因为可能导致冲突
。- 这里我们没有包含
<stdio.h>
,也可以使用printf
和scanf
,在包含<iostream>
后间接包含了。VS 系列编译器是这样的,其他编译器可能会报错。
C++输入流和输出流的使用
流提取(<<)(输入)
- 操作:使用
>>
操作符,你可以从std::cin
中读取不同类型的数据,如整数、浮点数、字符、字符串等。- 理解:我们可以理解为,输入到io流里面,比如是cin,然后从输入流中读取数据
- 例子:
#include<iostream>//头文件 int number; std::cin >> number; // 从标准输入读取一个整数
流插入(<<)(输出)
- 操作:使用
<<
操作符,你可以向std::cout
中写入数据,如文本、变量的值等。- 理解:我们可以理解把数据插入到io流里面,比如是cout,然后通过这个流将数据输出到屏幕或其他输出设备。
- 例子:
#include<iostream>//头文件 int number = 42; std::cout << number << std::endl; // 向标准输出写入一个整数和一个换行符
std::endl
std::endl
是一个操纵符,用于向输出流中插入一个换行符,并刷新输出流。它通常与流插入操作符<<
一起使用。
- 操作:当你向
std::cout
使用std::endl
时,它会输出一个换行符,并将输出流缓冲区的内容刷新到标准输出设备。std::cout << "Hello, World!" << std::endl; // 输出文本并换行
关系
std::cin
使用流提取操作符>>
来从输入流中读取数据。std::cout
使用流插入操作符<<
来向输出流中写入数据。std::endl
通常与std::cout
一起使用,用于在输出后添加换行并刷新输出流。
命名空间的展开(
using namespace std
)在C++中,命名空间(namespace)是一种封装全局标识符(如变量名、函数名、类名等)的方式,以防止不同代码库中的同名标识符发生冲突。
std
是C++标准库的命名空间,它包含了标准库中的所有实体。(简单说就是,std::cout
、std::cin
和std::endl
都是std
命名空间中定义的,你不想去域里面访问,直接取消就可以)代码
//代码不展开 #include<iostream>//头文件 int main() { //代码不展开 std::cout << "why" << std::endl;//打印出来why return 0; } //代码展开 #include<iostream>//头文件 using namespace std;//取消空间的折叠 int main() { cout << "why" << endl;//打印出来why return 0; }
C++缺省参数
缺省参数的概念
- 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省
缺省参数的具体实现
注意:缺省参数就是不需要的时候没啥用,需要的时候才出现,所以缺省参数就类似于舔狗
缺省参数注意事项以及实现
- 半缺省需要注意,这里的参数给是从右往左给参数的,但是传递参数的时候,是从左往右进行传递的,目的就是为了区分传递参数和缺省参数
- 声明和定义不能同时给(只能在声明里面给缺省值),因为万一声明给4,定义给10,那么以谁为基础?(那就是在函数的定义中,而不是在函数的声明中。)
C++函数重载
函数重载概述
概念: C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。
函数重载需要满足以下条件(防止调用歧义)
重点
- 函数名称:相同。
- 参数类型:必须不同。
- 参数数量:可以相同,也可以不同。
- 参数名称:即使不同,也不会影响重载的判断。
- 作用域:不在一个作用域,怎么都可以
函数重载举例1:
这个答案选择是D,原因,参数类型一样,所以不构成函数重载
以下不是double compare(int,int)的重载函数的是( ) A.int compare(double,double) B.double compare(double,double) C.double compare(double,int) D.int compare(int,int)
函数重载举例2:
这两个构成函数重载,都可以调用成功,并且返回,这里编译器会自动调用
C++引用(&)
引用的概念(类型& 引用别名 = 引用对象;)
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。比如:水壶传中李,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头;
引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
引用使用的目的
(这里讲的不深入,还是可以理解的,后面到拷贝构造的时候,还会涉及到深入的引用返回,传值返回,等深入的讲解)
引用传参
- 减少拷贝:通过引用传递参数,可以直接操作原始对象,避免了对象拷贝的开销,这对于大型对象或资源密集型对象尤其重要。
- 简化代码:引用传参使得函数调用更加直观,不需要像指针那样进行解引用操作。
- 与指针传参的比较:引用传参与指针传参在功能上类似,都能避免对象拷贝,但引用的语法更简洁,使用更方便。
引用返回值
- 避免拷贝:返回引用可以避免返回大型对象时的拷贝开销,直接返回对象的引用。
- 场景复杂:引用返回值的使用场景比较复杂,需要考虑对象的生命周期和所有权问题,这些内容在后续的类和对象章节中会深入讲解。
引用与指针的关系
- 相辅相成:引用和指针在某些功能上重叠,但它们各有特点,不能互相替代。
- C++引用的特点:C++中的引用一旦被初始化后就不能改变指向,这与Java等其他语言中的引用不同。
别名的使用:
#include<iostream> #include<stdio.h> int main() { //引用的使用(可以别名取别名) cout << "引用的使用:" << endl; int a = 10; int& b = a; int& c = b; int& d = c; cout << "&a=" << &a << endl; cout << "&b=" << &b << endl; cout << "&c=" << &c << endl; cout << "&d=" << &d << endl << endl; return 0; }
参数传递的使用:
#include<iostream> #include<stdio.h> //引用的使用 void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { //引用(参数传递的使用) int x = 1; int y = 2; cout << "x=" << x << " " << "y=" << y << endl; Swap(x, y); cout << "x=" << x << " " << "y=" << y << endl; return 0; }
局部变量不适合返回引用
当创建的对象是局部变量的时候,不适合引用,因为地址会进行销毁,此时引用会返回一个错误地址
C++ const引用(这里不讲返回,只是讲引用,拷贝构造篇章会有更详细的讲解)
使用规则
引用常量对象:可以引用一个常量对象,但必须使用常量引用(
const
引用)。这是因为常量引用保证了不会通过引用来修改被引用的对象。常量引用的灵活性:常量引用不仅可以引用常量对象,也可以引用普通(非常量)对象。这是因为引用的权限可以缩小(即常量引用可以绑定到非常量对象),但不能放大(即非常量引用不能绑定到常量对象)。
引用临时对象:在某些表达式中,如
int& rb = a * 3;
或int& rd = static_cast<int>(d);
,表达式的结果可能被存储在临时对象中。这些临时对象是未命名的,由编译器创建用于存储表达式的结果。临时对象的常性:C++标准规定,临时对象是常量,因此不能通过非常量引用来引用它们。只有常量引用可以绑定到临时对象。
权限放大问题:尝试通过非常量引用来引用临时对象会导致编译错误,因为这相当于权限放大,违反了C++的引用规则。
临时对象的定义:临时对象是编译器为了存储表达式的求值结果而创建的未命名对象。它们在表达式结束后通常会立即销毁,但通过常量引用可以延长其生命周期。
C++ const引用,权限放大问题
权限放大是问题,因为是不允许权限放大的
const会修饰的变量只能是可读的
#include<iostream> #include<stdio.h> int main() { //不能进行权限放大,但是我们可以进行权限平移 const int& a22 = a11; cout << "a22=" << a22 << endl << endl; cout << "&a11=" << &a11 << endl << "&a22=" << &a22 << endl << endl;//此时我们发现,地址依旧是一样的,因为这是一个别名的引用 return 0; }
C++ const引用,权限缩小
权限缩小不是问题,因为是允许权限缩小的
#include<iostream> #include<stdio.h> int main() { //权限的缩小 int a33 = 10; ++a33; cout << "a33=" << a33 << endl; const int& a44 = a33;//此时权限缩小 //++a44;//此时报错,因为权限缩小之后,内容不可修改 cout << "&a33=" << &a33 << endl << "&a44=" << &a44 << endl << endl; return 0; }
单纯的拷贝不存在权限问题
这里就是一个单纯的拷贝,没有引用,不存在权限问题
const可以给常量取别名
给常量取别名的意义
- 保护数据:通过使用
const
引用,可以确保函数不会意外修改传入的常量参数。- 提高效率:对于大型的数据结构,使用
const
引用作为函数参数可以避免不必要的拷贝,提高程序的运行效率。- 延长生命周期:对于临时对象,使用
const
引用可以延长其生命周期,使其在引用的生命周期内保持有效。const int original = 10; // 定义一个常量 const int& ref = original; // 用const引用给常量取别名
const int& refToTemp = 100; // 正确:临时对象可以绑定到const引用 // int& nonConstRef = 100; // 错误:临时对象不能绑定到非const引用
const引用(类型转化导致产生临时变量)
在C++中,"类型转换产生临时变量" 指的是在某些表达式中,由于类型不匹配,编译器会创建一个临时对象来存储表达式的结果,以便能够进行后续的操作。这个临时对象通常是一个右值(r-value),它只能被绑定到 const 引用上。
注意(后面拷贝构造会进行讲解):
- 拷贝也会产生临时对象
- 传值返回,表达式的结果中间都会产生临时对象
引用和指针的区别(面试概念性题型)
概念概述
内存占用:
- 引用:引用一个变量时,实际上并不占用额外的内存空间,它就是原始变量的别名。
- 指针:指针变量需要占用内存空间来存储一个地址值。
初始化:
- 引用:引用必须在定义时立即初始化,且一旦初始化后,不能再引用另一个对象。
- 指针:指针变量在定义时可以不初始化,但为了避免指向随机内存,建议初始化。
指向性:
- 引用:一旦引用了一个对象,就不能更改为引用另一个对象。
- 指针:指针可以随时更改其指向的对象。
访问对象:
- 引用:通过引用访问对象时,可以直接使用引用名,就像使用原始变量一样。
- 指针:通过指针访问对象时,需要使用解引用操作符
*
。
sizeof
操作符:
- 引用:
sizeof
引用的结果通常是引用类型的大小,即它引用的对象的大小。- 指针:
sizeof
指针的结果在任何平台上都是固定的,32位系统下通常是4个字节,64位系统下通常是8个字节。空值和野指针:
- 引用:引用很少出现空引用的问题,因为它们必须在创建时绑定到一个有效的对象。
- 指针:指针可以是空的(不指向任何对象),也可以是野指针(指向未分配或已释放的内存),这可能导致程序出错。
安全性:
- 引用:由于引用的这些特性,它们在使用上相对更安全,不容易出现指针的错误。
- 指针:指针的错误使用可能导致内存泄漏、程序崩溃等问题。
使用场景:
- 引用:常用于函数参数和返回值,以避免拷贝大型对象,或者当需要保证函数不会改变参数值时。
- 指针:用于动态内存分配、复杂的数据结构、底层系统编程等场景。
inline内联函数
概念概述
- 用
inline
修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。展开的意思就是不建立栈帧了,直接进行执行,所以本质就是提升效率用的inline
对于编译器而言只是一个建议,也就是说,你加了inline
编译器也可以选择在调用的地方不展开,不同编译器关于inline
什么情况展开各不相同,因为C++标准没有规定这个。inline
适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline
也会被编译器忽略。- C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了
inline
目的就是为了替代C的宏函数。- VS编译器 debug版本下面默认是不展开
inline
的,这样方便调试,debug版本想展开需要设置一下以下两个地方。inline
不建议声明和定义分离到两个文件(所以往往直接定义在头文件),分离会导致链接错误。因为inline
被展开,就没有函数地址,链接时会出现报错。补充说明
内联展开与栈帧:
- 内联函数的展开并不意味着完全不建立栈帧,而是减少了函数调用的额外开销。如果内联函数中涉及复杂的操作,如循环或条件分支,编译器可能会决定不进行内联展开,或者在某些情况下,内联展开可能仍然需要栈帧来保存局部变量和寄存器。
编译器的
inline
决策:
- 编译器会根据函数的大小、复杂度、调用频率以及优化级别等因素来决定是否进行内联展开。通常,小而简单的函数更可能被内联展开。(也就是如果内联的复杂度过高,会直接展开内联)。
- 展开内联函数并不意味着函数的代码会被移动到实现文件(.cpp文件)中。实际上,内联函数的定义通常位于头文件(.h或.hpp文件)中,这样编译器在处理每个包含该头文件的源文件时,都能够看到函数的定义,并在每个调用点进行展开。
宏函数与
inline
:
- 宏函数在预处理器阶段进行文本替换,不进行类型检查,可能导致难以调试的错误。
inline
函数提供了类型安全和调试支持,是宏函数的现代替代品。Debug模式下的
inline
:
- 在Debug模式下,为了便于调试,编译器可能会选择不内联展开函数。如果需要在Debug模式下进行内联展开,可能需要在编译器设置中明确启用。
声明与定义的一致性:
- 对于
inline
函数,建议将声明和定义放在同一个文件中,以避免链接时的地址不一致问题。如果声明和定义分离,可能会导致链接器找不到函数定义的地址,从而引发链接错误。内联函数的使用(不一定非要加上inline)
类内定义的成员函数:如果一个成员函数的定义位于类定义的内部,即使没有使用
inline
关键字,编译器通常也会尝试将其作为内联函数处理。模板函数:模板函数在某些编译器中默认会被当作内联函数处理,因为它们在编译时需要具体化,这与内联展开的过程相似。
编译器的自动内联:一些编译器可能会根据其优化策略自动决定内联某些没有显式
inline
关键字的函数,尤其是那些短小且调用频繁的函数。编译器特定的扩展:某些编译器可能提供了它们自己的内联函数扩展,这些扩展可能不要求使用
inline
关键字。将函数定义放在头文件中,并加上
inline
关键字,是创建内联函数的常见做法。这种做法有两个主要目的:
实现1:(这里是放到了类里面,也就是加不加inline其实都是内联函数)
这里的void print();就是内联函数
//.h //类 class Myclass { public: //构造函数(进行初始化) Myclass(int a = 1, int b = 2, int c = 3)// 缺省参数(全缺省) { _a = a; _b = b; _c = c; } //析构函数(进行释放空间)(没有资源不进行释放) //~Myclass(); //内联函数(inline)(不需要多次调用,直接放到类里面),此时就变成内联函数 inline void print() { cout << _a << "/" << _b << "/" << _c << endl; } private: int _a; int _b; int _c; }; //.cpp int main() { // 缺省参数(全缺省),包括全缺省,半缺省,无参数缺省参数 //cout << Myclass::Myclass << endl; Myclass m1;//构造初始化之后, m1.print();//直接进行打印 return 0; }
实现2:
//.h文件 //内联函数 inline void _print() { cout << "内联函数" << endl; } //。cpp文件 int main() { _print(); return 0; }
nullptr
概念概述:
在C++中,
nullptr
和NULL
都是用来表示空指针,但它们之间有一些重要的区别:
nullptr和NULL之间的区分
nullptr
:
nullptr
是C++11标准引入的一个关键字,用于表示空指针。- 它是类型安全的,意味着它必须被转换为一个指针类型才能使用,这可以防止一些类型错误。
nullptr
可以被隐式转换为任何指针类型,包括对象指针、函数指针、成员指针等。- 使用
nullptr
可以提高代码的可读性和可维护性。
NULL
:
NULL
是一个宏,通常在<cstddef>
或<cstdlib>
头文件中定义为0
或(void*)0
。- 它不是类型安全的,因为它本质上是一个整数常量,可以被赋值给任何指针类型,这可能导致类型错误。
NULL
可以被隐式转换为任何指针类型,但这种隐式转换可能会隐藏类型错误。最本质的区别就是:所以为了解决C++引入了新的关键字,nullptr(这里是不能转化成整形的)
标签:函数,int,namespace,基础,C++,引用,命名,入门 From: https://blog.csdn.net/Jason_from_China/article/details/140334726nullptr的使用
#include<iostream> //nullptr(空指针)(不能转化为整形)和null(可以转化为整形) void _null(int x) { cout << "_null(int x)=" << x << endl; } void _nullptr(int* x) { cout << "_nullptr(int x)=" << x << endl << endl; } int main() { //nullptr(空指针)(不能转化为整形)和null(可以转化为整形) //int a1 = nullptr; cout << "nullptr(空指针)(不能转化为整形)和null:" << endl; _null(NULL); _nullptr(nullptr); return 0; }