一、从 C 到 C++
1、引用(掌握)
1.1 概念
- 别名机制:引用本质上是对变量的一种别名,它就像变量的另一个名字,对引用的操作实际上就是对原变量的操作。从底层实现来看,引用可能是通过指针来实现的,但在使用上,它比指针更加直观和安全。引用在很多面向对象的编程语言中都有广泛应用,它提供了一种简洁而高效的方式来操作变量。
- 示例:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// b是a的引用
int &b = a;
cout << a << " " << &a << endl;
cout << b << " " << &b << endl;
return 0;
}
在这个示例中,b
是 a
的引用,输出结果可以看到 a
和 b
的值相同,并且它们的内存地址也相同,这表明它们指向同一个内存空间。
1.2 引用的性质
- 初始化要求:
- 必须初始化:声明引用时,必须同时进行初始化,因为引用一旦被创建,就必须绑定到一个具体的变量上。例如:
cpp
// 错误:未初始化
// int &b;
- 不能指向
NULL
:普通引用不能初始化为NULL
,因为引用必须引用一个实际存在的对象。例如:
cpp
// 错误:常量不能直接起别名
// int &b = NULL;
- 常量引用:
- 当使用
const
修饰引用时,它可以绑定到字面量,例如const int &b = 12;
。这种引用被称为常量引用,它的值不能被修改。这在函数参数传递中非常有用,当函数不需要修改传入的参数时,可以使用常量引用,这样既可以避免参数的拷贝,又能保证参数的安全性。 - 虽然常量引用本身的值不能被修改,但如果原变量的值被改变,引用的值也会相应改变。例如:
- 当使用
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 2;
const int &b = a;
// 错误:b是只读的
// b++;
a++;
cout << a << " " << &a << endl;
cout << b << " " << &b << endl;
return 0;
}
- 指针与引用:
- 可以将变量引用的地址赋值给一个指针,此时指针指向的还是原来的变量。例如:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int &b = a;
int *c = &b; // c同时指向了a和b
cout << a << " " << &a << endl;
cout << b << " " << &b << endl;
cout << *c << " " << c << endl;
return 0;
}
- 还可以对指针建立引用,例如
int *&d = c;
,这里d
是c
的引用,对d
的操作等同于对c
的操作。 - 不可重新绑定:引用一旦绑定到某个变量,就不能再绑定到其他变量。例如:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 1;
// b是a的引用
int &b = a;
int c = 3;
b = c; // 赋值,不是引用,b还是a的引用
b++;
cout << a << " " << &a << endl;
cout << b << " " << &b << endl;
cout << c << " " << &c << endl;
return 0;
}
在这个例子中,b = c;
只是将 c
的值赋给了 b
(也就是 a
),而不是让 b
成为 c
的引用。
1.3 引用的参数
- 传引用优势:在函数参数传递时,使用引用可以避免参数的拷贝,从而提高程序的运行效率。特别是当传递的对象比较大时,这种效率提升更为明显。例如,交换两个变量的值的函数,如果使用值传递,会创建两个变量的副本,而使用引用传递则可以直接操作原变量。
cpp
#include <iostream>
using namespace std;
// C++编程方式,符合需求
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a1 = 1;
int b1 = 2;
swap(a1, b1);
cout << a1 << endl;
cout << b1 << endl;
return 0;
}
- const 修饰:如果在函数中不打算修改传入的引用参数,建议使用
const
进行修饰,这样可以保证引用的安全性。例如:
cpp
#include <iostream>
using namespace std;
void printValue(const int &a)
{
// 错误:const修饰,无法修改
// a++;
cout << a << endl;
}
int main()
{
int a1 = 1;
printValue(a1);
cout << a1 << endl;
return 0;
}
面试题:指针与引用的区别?
- 初始化要求:引用必须在声明时进行初始化,而指针可以先声明,之后再赋值,也可以初始化为
NULL
。 - 重新绑定能力:引用一旦绑定到某个变量,就不能再重新绑定到其他变量;而指针可以在任何时候指向其他地址。
- 本质区别:引用是变量的别名,它本身并不是一个独立的对象;而指针是一个独立的变量,它存储的是另一个对象的地址。
- 内存占用:
sizeof(引用)
返回的是引用所绑定的变量的大小,而sizeof(指针)
返回的是指针本身的大小(通常在 32 位系统上是 4 字节,在 64 位系统上是 8 字节)。 - 使用安全性:引用在使用时不需要进行解引用操作,因此使用起来更加直观和安全;而指针在使用时需要进行解引用操作,如果不小心使用了空指针或野指针,可能会导致程序崩溃。
2、赋值(熟悉)
- 初始化语法:
- 传统赋值:
int a = 1;
这是最常见的赋值方式,在 C 和 C++ 中都可以使用。 - 括号初始化:
int a(1);
、int b(a);
这种方式在功能上等同于传统赋值,但在某些情况下,括号初始化可以避免一些潜在的问题,例如在模板编程中。
- 传统赋值:
cpp
#include <iostream>
using namespace std;
int main()
{
int a(1); // 等同于 int a = 1;
cout << a << endl;
int b(a); // 等同于 int b = a;
cout << b << endl;
int c(a + b); // 等同于 int c = a + b;
cout << c << endl;
return 0;
}
- C++11 列表初始化:
int b3{b};
这种初始化方式在 C++11 中引入,它对数据窄化(例如将double
类型的值转换为int
类型)会提出警告。这有助于避免一些潜在的数据丢失问题。
cpp
#include <iostream>
using namespace std;
int main()
{
int a(1); // 等同于 int a = 1;
cout << a << endl;
double b = 3.14;
int b1 = b;
cout << b1 << endl; // 3
int b2(b);
cout << b2 << endl; // 3
int b3{b}; // 升级:对数据窄化提出警告
cout << b3 << endl; // 3
return 0;
}
3、键盘输入(熟悉)
- 基本输入:
cin >> a >> str;
可以使用cin
从标准输入读取数据,并将其赋值给变量。cin
会自动根据变量的类型进行数据的转换。这种方式可以连续读取多个数据,数据之间用空格分隔。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
int a;
// C++的字符串是string
string str;
cout << "请输入一个数字和字符串" << endl;
cin >> a >> str; // 接收键盘输入,一个整数和一个字符串,可以连续操作
cout << a << str << endl;
return 0;
}
- 带空格字符串:当需要读取包含空格的字符串时,使用
getline(cin, str);
可以读取整行输入,包括空格。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
// C++的字符串是string
string a;
cout << "请输入一个字符串,可以包含空格" << endl;
getline(cin, a);
cout << a << endl;
return 0;
}
4、string 字符串类(掌握)
- 头文件:使用
string
类需要包含头文件#include <string>
,注意不是string.h
,string.h
是 C 语言中用于处理字符串的头文件。string
类是 C++ 标准库中的一个类,它提供了方便的字符串操作功能,并且可以自动管理内存,避免了手动管理内存的麻烦。 - 常用方法:
- 长度获取:
str.size()
和str.length()
都可以返回字符串的长度,它们的功能是相同的。 - 字符访问:可以使用
str[i]
和str.at(i)
来访问字符串中的第i
个字符。str[i]
的执行效率较高,但不进行越界检查;而str.at(i)
会进行越界检查,如果越界会抛出异常,因此更加安全。
- 长度获取:
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "helloworld";
cout << str.size() << endl;
cout << str.length() << endl;
cout << str[5] << endl;
cout << str.at(5) << endl;
return 0;
}
- 遍历方式:
- 普通循环:使用
for
循环可以遍历字符串中的每个字符,例如:
- 普通循环:使用
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "helloworld";
// 以for循环的方式进行输出字符串
for (int i = 0; i < str.size(); i++)
{
cout << str.at(i);
}
cout << endl;
return 0;
}
- C++11 范围循环:C++11 引入了范围循环,使用
for (char c : str)
可以更简洁地遍历字符串中的每个字符。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "helloworld";
// 以for each的方式循环遍历字符串
for (char c : str)
{
cout << c;
}
cout << endl;
return 0;
}
- 类型转换:
- 字符串转数字:可以使用
istringstream
来将字符串转换为数字。例如:
- 字符串转数字:可以使用
cpp
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
string s = "123";
// string → int
istringstream iss(s);
int i;
iss >> i;
cout << i << endl;
return 0;
}
- 数字转字符串:使用
stringstream
可以将数字转换为字符串。例如:
cpp
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
int i = 123;
// int → string
stringstream ss;
ss << i;
string s2 = ss.str();
cout << s2 << endl;
return 0;
}
5、函数
5.1 内联函数(熟悉)
- 作用:内联函数的主要作用是取代 C 语言中宏定义的函数。在编译时,内联函数会直接将函数体展开到调用它的地方,这样可以减少函数调用的开销,提高程序的执行效率。特别是对于那些代码简短、频繁调用的函数,使用内联函数可以显著提高性能。
- 建议场景:通常将具有以下性质的函数写为内联函数:
- 代码长度在 5 行以内,这样可以保证函数体不会过于庞大,展开后不会导致代码膨胀。
- 不包含复杂的控制语句,如
for
循环、while
循环、switch
语句等,因为复杂的控制语句会增加代码的复杂度,不适合内联展开。 - 频繁被调用的函数,这样可以充分发挥内联函数减少调用开销的优势。
- 关键字:
inline
是声明内联函数的关键字。需要注意的是,手动添加inline
关键字只是给编译器一个建议,编译器会根据自己的判断准则来决定是否将函数真正内联。例如:
cpp
#include <iostream>
#include <string>
using namespace std;
// 内联函数
inline void print_string(string str)
{
cout << str << endl;
}
int main()
{
print_string("helloworld");
return 0;
}
5.2 函数重载(重点)
- 条件:C++ 中允许使用函数重载,即多个函数可以使用同一个名称。函数重载的条件是函数名称相同,但参数列表不同,具体可以是参数的类型不同、数量不同或者参数的前后顺序不同。需要注意的是,函数重载与返回值类型无关。例如:
cpp
#include <iostream>
#include <string>
using namespace std;
void print_show(int i)
{
cout << "调用了int重载:" << i << endl;
}
void print_show(float i)
{
cout << "调用了float重载:" << i << endl;
}
void print_show(string i)
{
cout << "调用了string重载:" << i << endl;
}
int main()
{
print_show(1);
print_show(1.2f);
print_show("hello");
return 0;
}
- 错误示例:仅返回值类型不同或仅
const
修饰参数(如const string
)无法实现函数重载,因为编译器在调用函数时,无法根据返回值类型或const
修饰来区分应该调用哪个函数。例如:
cpp
// 错误:名称相同,参数类型相同,编译器无法区分
// 与返回值类型无关
// int print_show(int s)
// {
// cout << "调用了int2重载:" << s << endl;
// return s;
// }
// 错误:const关键字,也无法作为判断依据
// void print_show(const string s)
// {
// cout << "调用了string重载:" << s << endl;
// }
5.3 哑元函数(熟悉)
- 定义:函数的参数只有类型,没有名称,这样的函数就是哑元函数。例如:
cpp
#include <iostream>
using namespace std;
// 哑元函数
void print_show(int)
{
cout << "调用了int重载" << endl;
}
int main()
{
print_show(1);
return 0;
}
- 作用:
- 区分函数重载:哑元函数可以用来区分函数重载,例如:
cpp
#include <iostream>
using namespace std;
// 哑元函数
void print_show(int a, double)
{
cout << "调用了int, double重载" << endl;
}
void print_show(int a)
{
cout << "调用了int重载" << endl;
}
int main()
{
print_show(1);
print_show(1, 2.0);
return 0;
}
- 运算符重载:在运算符重载中,哑元函数也有重要的应用。例如,在重载后置自增运算符
++
时,通常会使用一个哑元参数来区分前置和后置自增运算符。