01.引用的使用场景(重点)
1. 引用作为函数参数
void func(int &a, int &b)
{
int sum = a + b;
cout << "sum=" << sum << endl;
}
void test01()
{
int a = 10;
int b = 20;
func(a, b);
}
在这个例子中,func 函数接受两个整数引用作为参数。通过使用引用,函数可以修改传递给它的实际参数的值,而不是创建它们的副本。这可以提高程序的效率,尤其是在处理大量数据时。
2.引用作为函数的返回值
int &func2()
{
int b = 10; // 注意1:不要返回局部变量的引用
int &p = b;
return p;
}
int &func3()
{
static int b = 10;
return b;
}
void test02()
{
int &q = func2();
q = 100;
cout << q << endl;
func2() = 200; // 上面的代码是错误,只是编译器没有检测出来
cout << q << endl;
cout << "func2=" << func2() << endl;
func3() = 100; // 注意2:如果要函数当左值,那么该函数必须返回引用
cout << "func3()=" << func3() << endl;
}
这部分代码演示了引用作为函数的返回值。函数 func2 返回一个对局部变量的引用,这是不安全的,因为局部变量在函数执行完毕后会被销毁。这种情况下,引用可能引发未定义的行为。函数 func3 返回了一个对静态变量的引用,这是安全的,因为静态变量的生命周期持续整个程序运行过程。
在 test02 中,展示了通过引用修改函数返回的值。需要注意的是,如果希望函数可以作为左值使用(如 func2() = 200;),则该函数必须返回引用。
引用作为函数参数可以方便地修改实际参数的值,而引用作为函数返回值则可以允许函数返回一个可修改的左值。
02.常量引用
示例:
int &ref = 10; // 错误
const int &ref2 = 10;// 正确
// 原理是: int tmp = 10; const int &ref = tmp;
1.int &ref = 10;: 这是错误的,因为引用 ref 是一个非常量引用,而右侧是一个临时的字面常量,这种情况下不允许将非常量引用绑定到字面常量上。引用需要引用一个可修改的内存,但字面常量是不可修改的。
2.const int &ref2 = 10;: 这是正确的。在这里,引用 ref2 是一个常量引用,可以绑定到字面常量。这是因为常量引用允许绑定到不可修改的数据。
注释:
int tmp = 10; // 创建一个临时变量 tmp,并赋值为 10
const int &ref = tmp; // 使用常量引用 ref 绑定到 tmp
在这里,我们创建了一个临时变量 tmp,然后使用常量引用 ref 将其引用。这样,通过常量引用,我们可以使用引用来访问 tmp 的值,并且由于 ref 是常量引用,不会允许修改其绑定的值。
03.内联函数(了解)
1. 宏函数的缺陷
#define ADD(x,y) x+y
//在普通函数前面加上inline是向编译器申请成为内联函数
//注意:加inline可能成为内联函数,可能不成为内联函数
inline int Add(int x, int y)
{
return x + y;
}
void test()
{
//10+20*2
int ref = ADD(10, 20) * 2;
cout << "ref=" << ref << endl;
int ref2 = Add(10, 20) * 2;
cout << "ref2=" << ref2 << endl;
}
在这里,ADD 是一个宏函数,宏函数的缺陷主要包括两方面:
1.潜在的不安全性: 宏是简单的文本替换,可能导致预期外的结果。例如,ADD(10, 20) * 2 会被替换为 10 + 20 * 2,可能与预期的 10 + (20 * 2) 不同。
2.可读性差: 宏不会进行参数类型检查,而且在代码中只是简单的文本替换,可读性较差。
2. 内联函数的使用和不成为内联函数的情况
#define COMAPD(x,y) ((x)<(y)?(x):(y))
inline int func(int x, int y)
{
return x < y ? x : y;
}
void test02()
{
int a = 1;
int b = 3;
// ((++a)<(b)?(++a):(b))
//cout << "COMAPD(x,y)=" << COMAPD(++a, b) << endl;//3
cout << "func=" << func(++a, b) << endl;//2
}
在这里,COMAPD 是一个宏,func 是一个内联函数。内联函数有以下好处:
- 效率高: 内联函数在编译时会被展开,避免了函数调用的开销,提高了执行效率。
2.类型安全: 内联函数会进行参数类型检查,避免了一些宏的问题。
不过,内联函数不是在所有情况下都会成为内联函数。在这个例子中,func 的调用 cout << "func=" << func(++a, b) << endl; 可能不会被编译器内联,具体原因可能是因为存在自增运算,而内联函数对于一些复杂操作或表达式不会进行内联优化。
3. 什么情况不会成为内联函数
内联函数是一种在编译时将函数调用处直接展开成函数体的优化手段,但并非所有情况都适合内联。以下是一些情况,其中哪些不适合内联函数:
- 存在过多的条件判断语句: 内联函数展开后,条件判断语句会直接嵌入调用处,如果条件判断较为复杂或过多,可能会导致内联代码过于庞大,影响代码的可读性和性能。
- 函数体过大: 内联函数的目的是减少函数调用的开销,如果函数体过大,内联展开后会增加代码的体积,可能导致代码膨胀,影响缓存性能。
- 对函数进行取址操作: 内联函数在编译时展开,取函数地址的操作可能不符合内联的特性,因为内联函数的地址可能不存在或不稳定。
4.存在任何形式的循环语句: 循环语句可能导致内联代码的重复展开,增加代码体积,而内联的初衷是减少函数调用的开销,循环体过大可能不适合内联。
4. 内联函数的好处
有宏函数的效率,没有宏函数的缺点: 内联函数在编译时展开,避免了函数调用的开销,类似于宏函数的效率,但又不会带来宏函数的一些缺点,如参数多次计算、缺少类型检查等。
类的成员函数默认加上 inline: 在类定义中声明的成员函数,默认被视为内联函数。这有助于在类的接口中定义简短的函数,减少调用开销。
5. 在普通函数前面加上 inline 是申请成为内联函数
在C++中,使用 inline 关键字在函数定义或声明前面加以修饰,就是在申请将该函数作为内联函数。这只是一种建议,编译器可以选择是否将该函数内联,具体的决策通常取决于编译器和代码的具体情况。
inline int myInlineFunction(int a, int b) {
return a + b;
}
在上述例子中,myInlineFunction 被标记为内联函数。但是,编译器可以根据具体情况选择是否真正内联该函数。
04. 函数的默认参数
1. 函数的默认参数概述
函数的默认参数是指给函数的形参赋予默认值。这在函数调用时允许不提供该参数,从而增加函数的灵活性。
int myFunc(int a, int b = 0) {
return a + b;
}
在上述例子中,b 被赋予了默认值 0,这样在调用 myFunc(10) 时,不提供第二个参数,将使用默认值。
2. 函数的默认参数的注意事项
1默认参数后面的参数必须都是默认参数: 在函数的声明或定义中,如果某个参数被赋予默认值,其后的参数也必须都有默认值。这是因为在调用时,省略某个参数,后面的参数必须也能够正确匹配。
// 正确示例
int myFunc2(int a, int b = 0, int c = 2, int d = 3) {
return a + b + c + d;
}
2.函数的声明和实现不能同时有函数的默认参数: 函数的声明和实现分开时,不能同时给出默认参数,否则会产生冲突。
// 错误示例
void myFunc3(int a, int b);
void myFunc3(int a, int b = 0) {
// ...
}
解决方法是只在声明或实现中给出默认参数,另一方面不要给出默认参数,或者两者都不给出默认参数,以保持一致。
05. 函数的占位参数
在 C++ 中,函数的占位参数(placeholder parameters)是一种特殊的参数,它没有具体的名称,仅仅是一个占位符。通常在函数的声明或定义中使用,其目的在于给函数的形参提供默认值。
示例 1: 占位参数与默认值
// 函数的占位参数,占位参数在后面运算符重载时区分前加加或后加加
void func(int a, int = 10) {
// ...
}
void test02() {
func(10);
}
在这个例子中,int = 10 是一个占位参数,它没有具体的名称,只是用于为函数提供默认值。在 test02 中调用 func(10),由于没有提供第二个参数,将使用占位参数的默认值 10。
示例 2: 占位参数与默认参数混搭
// 占位参数和默认参数混搭
void func2(int = 10, int a = 20) {
// ...
}
void test03() {
func2(); // 使用默认值 10 和 20
func2(10); // 使用提供的值 10,而 a 使用默认值 20
func2(10, 30); // 使用提供的值 10 和 30
}
在这个例子中,int = 10 是占位参数,而 int a = 20 是默认参数。占位参数和默认参数可以一起使用,但需要注意的是,占位参数通常在默认参数之前。
占位参数的主要用途之一是为运算符重载提供不同的语法。例如,在后置递增运算符的重载中,就可以使用占位参数来区分前加加和后加加的语法。
占位参数是为了提供更灵活的函数调用方式,使得函数在被调用时可以有不同的默认值,同时为一些特殊的语法情况提供支持。
06.函数重载(重点)
1. 函数重载概述
函数重载是指允许在同一作用域内定义多个函数,这些函数具有相同的名称但具有不同的参数列表。
2. 函数重载的目的
函数重载的主要目的是为了方便使用函数名,使得在同一作用域内可以有多个功能相似但参数不同的函数。
3. 函数重载的条件
函数重载的条件包括:
1.参数个数不同: 函数可以重载,只要它们的参数个数不同。
2.参数类型不同: 函数可以重载,只要它们的参数类型不同。
3.参数顺序不同: 函数可以重载,只要它们的参数顺序不同。
4. 调用重载函数的注意事项
在调用重载函数时,编译器会进行严格的类型匹配。如果类型不匹配,编译器会尝试进行类型转换。如果转换成功,就调用相应的函数;如果失败,会导致编译错误。
5. 函数重载与默认参数的二义性问题
当函数重载和默认参数一起使用时,可能会出现二义性问题。编译器可能无法确定要调用哪个函数,尤其是当函数调用提供的参数和默认参数都能匹配时。
6. 函数返回值不作为函数重载条件
函数的返回值类型不影响函数重载。编译器主要通过调用函数时传入的参数来判断调用的是哪个重载函数。
这些规则和注意事项为理解和使用函数重载提供了基本的指导。
07. 函数重载的原理
- 原理概述函数重载的原理是在汇编时给各个函数取别名。这意味着具有相同函数名但不同参数的函数在汇编层面会有不同的名称,以便区分它们。C语言不能进行函数重载的原因是因为在C中,函数名没有被取别名的机制。
- 生成汇编文件: 使用gcc或g++可以生成对应的汇编文件。例如,gcc -S test.c -o test.s 会将C语言源文件test.c编译成汇编文件test.s,而g++ -S test.cpp -o test2.s 则会将C++源文件test.cpp编译成汇编文件test2.s。
3.查看内容: 使用type 文件名命令可以查看文件的内容。这个命令在Windows命令行中使用,对于Unix/Linux系统,可以使用cat 文件名或less 文件名等命令。
08. C++调用C语言的函数
问题描述: 当C++调用C语言的函数时,由于C++会在汇编时给函数取别名,而C语言不会,可能导致调用出错。
1.解决方法: 为了解决这个问题,可以在C++代码中使用extern "C"关键字来声明C语言的函数。这样告诉C++编译器在链接时按照C语言的方式去寻找函数,避免了因为取别名导致的错误。
// test.h 文件
#ifdef __cplusplus
extern "C" {
#endif
void func(); // C语言的函数声明
#ifdef __cplusplus
}
#endif
上述代码中,extern "C"告诉C++编译器,下面的函数以C语言的方式去寻找。这样,在C++中调用C语言的函数就不会出现链接错误。
09. 类和对象的概念
1.类的定义: 类是一种自定义数据类型,它是C语言的结构体(struct)进化而成的。在C++中,类不仅包含数据成员(类似于结构体的成员变量),还可以包含成员函数(类似于结构体中的函数指针)。
2.对象的定义: 对象是类的实例化结果,通过使用类来定义变量即可创建对象。在给定的例子中,Maker是一个类,而m就是Maker类的一个对象。
10. 类的封装
1.封装概述: 封装是将类的属性(变量)和方法(函数)封装到类内部,并为这些数据赋予访问权限。
2.为什么要有封装:
1.防止乱调用函数和变量,避免错误。
2.代码维护更方便。
3.权限:
公有权限(public): 可以被类的外部访问。
私有权限(private): 只能在类的内部访问。
保护权限(protected): 类的内部和派生类可以访问。
class Maker
{
public: // 公有权限
void set(string Name, int Id)
{
id = Id;
name = Name;
}
void printMaker()
{
cout << "id=" << id << " name=" << name << endl;
}
private: // 私有权限
int id;
string name;
protected: // 保护权限
int a;
};
// 继承
class Son : public Maker
{
void func()
{
a = 20; // 子类的类内可以访问父类的保护权限的成员
}
};
4.封装的优势:
1.控制属性的读写权限。
2.提供客户端访问数据的一致性。
3.保护属性的合法性。
11. 类和结构体的区别
在C++中,类和结构体的区别主要在于默认的访问权限:
1.结构体(struct): 默认权限是公有的。
2.类(class): 默认权限是私有的。
这意味着在结构体中,成员变量和成员函数默认是公有的,而在类中,默认情况下它们是私有的。