C++基础知识
引用
(一)引用概念
引用不是定义一个新的变量,而是给已存在的变量取了一个别名,编译器不会给引用变量开辟一块新的内存空间,而是引用变量和原变量共用同一块内存空间。
引用形式:
类型& 引用变量名=引用实体
int main() {
int a = 10;
int& p = a; //定义引用类型
cout << &p << endl;
cout << &a << endl;
//指针取别名
int* b = &a;
int*& p1 = b;
cout << &b << endl;
cout << &p1 << endl;
return 0;
}
注意:引用类型必须和引用实体是同一个类型。
引用类型p1和a是不同类型,所以编译器会报错。
(二)引用的特性
1.引用在定义时必须初始化。
2.一个变量可以有多个引用。(多个引用和变量都共用同一块空间)
int main() {
int a = 10;
int& p = a;
int& p1 = a;
return 0;
}
3.一个引用一旦引用一个实体就不能再引用其他实体。
(三)常引用
加上const就是常量,权限可以缩小,但是不能放大。
(四)引用使用
1.做参数
int Add(int& x, int& y) {
return x + y;
}
引用做参数,对形参的更改就是对外部实参的更改。
2.做返回值
首先了解传值返回:
int Add(int x, int y) {
return x + y;
}
int main() {
int ret=Add(3, 5);
cout<<ret<<endl;
return 0;
}
传值返回:函数调用时,返回值为函数结果的临时拷贝。函数返回值在调用完之后不会随栈帧销毁而销毁,返回值在栈帧销毁之前被保存在某个寄存器。
传引用返回:
上面使用ret接受,int&作为返回地址, 返回值c就是ret的别名,下面使用int& ret接受,int&作为返回值,但是为什么上面结果正确,下面结果错误在哪呢?
上面调用Add函数将c的别名返回后给ret接受,Add函数运行结束后,对应的栈空间就被回收了,但是内存还在,还没有被置为随机值,如果栈帧销毁了,返回就是随机值,ret的结果是未定义的,这种引用返回不正确的。
下面调用ret的值可能是随机值也可能不是,取决于这块空间有没有被重新覆盖和使用。cout函数栈帧的创建覆盖了Add函数的函数栈帧,所以ret的值变成随机值了。
所以正确的使用方式是静态变量,全局变量,上一层的栈帧或者malloc等等
int& Add(int x, int y) {
static int c = x + y;
return c;
}
int main() {
int& ret = Add(2, 3);
cout << "Add(2,3)的值为:" << ret << endl;
return 0;
}
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以引用返回,如果已经还给系统了,则必须使用传值返回。
(五)传值和传引用效率比较
以值作为参数或者返回值类型时,在传参和函数返回,函数不会直接传递实参或者将变量本身返回,而是传递实参或者返回变量的一份临时拷贝,所以用值作为参数或者返回值类型,效率会非常低下。
1. 以值或引用作为函数参数比较
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main() {
TestRefAndValue();
return 0;
}
2. 值和引用作为返回值类型的比较
#include <time.h>
struct A { int a[10000]; };
//全局变量A
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "以值作为返回值"<<"TestFunc1 time:" << end1 - begin1 << endl;
cout << "以引用作为返回值"<< "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
(六)引用和指针的区别
1.引用是变量的别名,没有独立空间,和引用实体共用同一个空间。而指针有独立的空间,指向变量地址。
2.引用在定义时必须初始化,而指针没有要求。
3.引用在初始化时引用一个实体后就不能再引用其他实体,而指针可以指向任何同一类型的实体。
4.在sizeof中含义不同,引用结果是引用类型的大小,而指针始终是地址空间所占字节数的个数(32位平台下占4个字节)。
5.没有NULL引用,但有NULL指针。
6.有多级指针,但没有多级引用。
7.引用自加是引用的实体加一,而指针自加是指针向后偏移一个类型的大小。
8.访问实体的方式不同,指针访问需要显示解引用,引用访问编译器自己处理。
内联函数
以inline修饰的函数叫内联函数,编译时C++编译器会在调用内联函数的地方展开,inline是一种以空间换时间的做法, 如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,优点就是少了调用开销,提高了运行效率。缺点就是将目标文件变大。
注意:inline不建议声明和定义分离,分离就会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
//Func.h
#include<iostream>
using namespace std;
inline void Func(int i);
//Func.cpp
#include"Func.h"
void Func(int i) {
cout << i << endl;
}
//main.cpp
#include"Func.h"
int main() {
Func(10);
return 0;
}
auto关键字
int main() {
std::map<std::string, std::string> m{ {"apple","苹果"}
,{"orange","橙子"},{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end()) {
//
}
return 0;
随着程序越来越复杂,程序的类型也越来越复杂,类型难以拼写或者含义不明确导致出错,std::map<std::string, std::string>是一个类型,这个类型太长,容易写错。有时会通过typedef给类型取别名:
typedef std::map<std::string, std::string> Map;
int main() {
Map m{ {"apple","苹果"},{"orange","橙子"},{"pear","梨"} };
Map::iterator it = m.begin();
while (it != m.end()) {
//
}
return 0;
}
但是typedef在编程时常常把表达式的值赋值给变量。
所以使用auto修饰变量,auto不再是一个存储类型的指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须是编译器在编译时推导而得。
auto在定义变量时必须初始化,在编译阶段需要根据初始化表达式来推导auto的实际类型,所以编译器将auto替换为变量的实际类型。
int TestAuto() {
return 1;
}
int main() {
int a = 10;
auto b = a; //int
auto c = 'a'; //char
auto d = TestAuto(); //int
//typeid(b).name()获取变量的实际类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
(二)auto的使用
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有什么区别,但用auto声明引用类型时则必须加&
int main() {
int x = 10;
auto a = &x; //int*
auto* b = &x; //int*
//auto* b = x; //编译失败,b限定为指针
auto& c = x; //int
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << *a << endl; //10
cout << *b << endl; //10
cout << c << endl; //10
return 0;
}
2. 在同一行可以定义多个变量
当在同一行定义多个变量时,这些变量必须是同一个类型,否则编译会报错。
(三)auto不能使用的场景
1.auto不能作为函数的参数
编译就会报错,auto不能作为形参类型,因为编译器不能对变量的实际类型进行推导。
2. auto不能用来声明数组
基于范围for循环
(for循环迭代的范围必须是确定的)
我们在学c时遍历一个数组,按照以下方式遍历:
int main() {
int a[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
cout << a[i] << " ";
}
return 0;
}
对于一个有范围的集合来说,循环范围是多余的,C++引入基于范围for循环:
for(范围内用于迭代的变量:被迭代的范围)
int main() {
int a[] = { 1,2,3,4,5 };
for (auto x : a) {
cout << x << " ";
} //自动依次取数组中数据赋值给x,自动判断结束。x相当于数组的拷贝。
return 0;
}