个人主页:C++忠实粉丝**
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创**C++基础入门(下)
收录于专栏【C++语法基础** 】
本专栏旨在分享学习C++的一点学习笔记,欢迎大家在评论区交流讨论💌**
目录
[2.1 引⽤的概念和定义](#2.1 引⽤的概念和定义)
[2.2 引⽤的特性](#2.2 引⽤的特性)
[2.3 引⽤的使⽤](#2.3 引⽤的使⽤)
[2.4 const引⽤](#2.4 const引⽤)
[2.5 指针和引⽤的关系](#2.5 指针和引⽤的关系)
1.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
1.1.参数类型不同
cpp
#include <iostream>
#include <cstdio>
using namespace std;
//1.参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
int main()
{
int result1 = Add(1, 2);
double result2 = Add(1.1, 2.2);
cout << result1 << " " << result2 << endl;
return 0;
}
1.2参数个数不同
cpp
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(1);
}
cpp
// 返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{}
int fxx()
{
return 0;
}
cpp
//下⾯两个函数构成重载
// f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
}
void f1(int a = 10)
{
}
使用命名空间可以解决这个问题,但是这样就不构成函数重载
cpp
namespace My_yu
{
void f1()
{
cout << "f()" << endl;
}
}
2.引用
2.1 引⽤的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋 ⻛";林冲,外号豹⼦头;
类型& 引⽤别名=引⽤对象;
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的>,这⾥引⽤也和取 地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。(吐槽⼀下,这个问题其实挺坑的,个⼈觉得⽤更多符号反⽽更好,不容易混淆)
cpp
#include<iostream>
#include<assert.h>
using namespace std;
int main()
{
int a = 0;
// 引用:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这里取地址我们看到是一样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
包括数据结构中的链表的实现
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, * PNode;
// typedef struct ListNode* PNode;
//避免了二级指针的使用
//void ListPushBack(LTNode** phead, int x)
void ListPushBack(LTNode*& phead, int x)
//void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
//PNode plist = NULL;
LTNode* plist = NULL;
ListPushBack(plist, 1);
return 0;
}
2.2 引⽤的特性
• 引⽤在定义时必须初始化
• ⼀个变量可以有多个引⽤
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 10;
// 编译报错:"ra": 必须初始化引用
//int& ra;
int& b = a;
int& c = b;
int d = 20;
b = d;
// 这⾥并⾮让b引⽤d,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值
cout << &a << endl;
cout << &b << endl;
cout << &d << endl;
return 0;
}
2.3 引⽤的使⽤
• 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
• 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• 引⽤返回值的场景相对⽐较复杂,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
• 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
• ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
cpp
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
cpp
#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
// 满了, 扩容
if (rs.top == rs.capacity)
{
printf("扩容\n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
void STModityTop(ST& rs, int x)
{
rs.a[rs.top-1] = x;
}
// int STTop(ST& rs)
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top-1];
}
int& func()
{
int a = 0;
return a;
}
int main()
{
// 调用全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
STModityTop(st1, 3);
STTop(st1) = 4;
return 0;
}
cpp
//错误示范
int& func()
{
int a = 0;
return 0;
}
返回引用类型与局部变量绑定:
函数
func()
声明返回一个int&
类型的引用,但在函数内部,将一个局部变量a
的地址返回。这是一个问题,因为局部变量在函数执行完毕后会被销毁,引用指向的内容将变得无效或者不可预测。返回类型不匹配:
函数声明返回
int&
,表示返回一个int
类型的引用。然而,在return
语句中返回的是整数0
,而不是一个具体的变量或者引用。这导致了返回类型与声明类型不匹配。
2.4 const引⽤
• 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
• 不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
• 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
cpp
int main()
{
//只读不可写
const int a = 10;
//权限进行了放大
int& rd = a;
return 0;
}
应该用const进行修改:
cpp
int main()
{
//只读不可写
const int a = 10;
//权限进行了放大
//int& rd = a;
const int& ra = a;
return 0;
}
cpp
//可以进行权限的缩小,但不能放大
int b = 20;
const int& rb = 20;
b++;
//rb++;
C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
cpp
const int& rc = 30;
const int& rd = (a + b);
double d = 12.34;
int i = d;
const int& ri = d;
//double& rd = d;
2.5 指针和引⽤的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
3.inline
• ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率。
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
cpp
// 实现一个ADD宏函数的常见问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
cpp
// 正确的宏实现
//#define ADD(a, b) ((a) + (b))
为什么不能加分号?
为什么要加外面的括号?
为什么要加里面的括号?
为什么不能加分号?
在宏定义中,一般情况下不应该在最后加分号。原因如下:
替换后的语句形式问题:如果在宏定义末尾加上分号,那么在使用宏时,宏展开后的语句就会变成一个单独的语句后面跟着一个空语句(即分号本身)。这可能导致不符合语法规则,或者在预期之外多出一个空语句的问题。
宏的使用方式:宏展开后,其结果会直接替换代码中的宏调用处,如果宏展开后包含分号,会破坏代码的结构,导致语法错误或者逻辑错误。
为什么要加外面的括号?
在宏定义中,为了避免运算符优先级和结合性带来的问题,外面加上括号是一个良好的习惯。例如,考虑这样的情况:
#define ADD(a, b) a + b
如果使用
ADD(3, 4) * 5
,宏展开后变成3 + 4 * 5
,这可能与预期的3 + (4 * 5)
不同,因为乘法运算符*
的优先级高于加法运算符+
。因此,加上外面的括号可以确保宏展开后整体的结构不会被其他运算符的优先级和结合性改变。为什么要加里面的括号?
在宏定义中,为了避免参数替换时带来的副作用或者歧义,需要在每个参数及其使用处加上括号。例如:
#define ADD(a, b) ((a) + (b))
这样做的好处包括:
- 参数的安全性:保证在替换时不会因为参数本身包含运算符或表达式而导致错误的替换结果。
- 表达式的完整性:确保宏展开后,每个参数和运算符的结合方式符合预期。
所以写一个正确的宏是一个很容易错误的事情,而我们的inline函数可以很好的解决
cpp
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
return ret;
}
int main()
{
int ret = Add(1, 2);
cout << Add(1, 2) << endl;
cout << Add(1, 2) * 5 << endl;
return 0;
}
inline函数进行了展开
注意:
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
cpp
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
cout << ret << endl;
return 0;
}
4.nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL
#else
#define NULL ((void*)0)
#endif
#endif
• C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的 f(int*)函数,但是由于NULL被定义成0,调⽤了f(intx),因此与程序的初衷相悖。f((void*)NULL); 调⽤会报错。
• C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换 成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被 隐式地转换为指针类型,⽽不能被转换为整数类型。
cpp
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
//f((void*)0);
f(nullptr);
// C++
void* p1 = NULL;
int* p2 = (int*)p1;
//int* p2 = nullptr;
//int i = nullptr;
return 0;
}