C++入门(二)
作者:小卢
专栏:《C++》
喜欢的话:世间因为少年的挺身而出,而更加瑰丽。 ——《人民日报》
1.引用
1.1.引用的概念及应用
引用(&)
引用不是新定义一个变量,而是给已存在变量取了一个别名
它和它引用的变量共用同一块内存空间
类型& 引用变量名(对象名) = 引用实体;
引用在定义时,必须初始化
一个变量可以多次引用
1.2.传值返回和传址返回和传引用返回的底层原理:
int Cout() { int n = 0; n++; return n; } int main() { int ret = Cout(); return 0; }
n是如何传给ret的呢?
因为这里的n没有用static修饰,为临时变量,Cout函数调用时开辟了一段栈帧,n存在于这段栈帧内。但函数调用结束后,栈帧销毁。
n可以传过去是因为,函数栈帧结束前用了一个临时变量=n,然后用这个临时变量来作为返回值给ret。
这个临时变量应该内存比较小的时候,是用寄存器。
int Cout() { static int n = 0; n++; return n; } int main() { int ret = Cout(); return 0; }
这种情况下,n在静态区,这里n还是用一个临时变量来作为中间段,来进行返回值。
这种情况有优化的空间:
这种返回类型为传值返回。那如果用传引用返回呢?
这种就是利用一些变量出了作用域过后还存在的情况,例如:引用,malloc…
int& Cout() { static int n = 0; n++; return n; } int main() { int ret = Cout(); return 0; }
引用返回的好处:
1.减少拷贝
2.调用者可以修改返回对象
//引用返回 //1.减少拷贝 //2.调用者可以修改返回对象 #define N 10 typedef struct Array { int a[N]; int size; }AY; int& PostAt(AY& ay, int i) { assert(i < N); return ay.a[i]; } int main() { //int ret = Cout(); AY ay; for (int i = 0; i < N; i++) { PostAt(ay, i) = i * 10; } for (int i = 0; i < N; i++) { cout << PostAt(ay, i) << " "; } cout << endl; return 0; }
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :" << ret << endl; //ret为随机值 return 0; }
这里ret为随机值,这里c返回的是一个别名,相当于返回的是一个c的别名,ret就是c的别名。
int main() { int a = 1; int& b = a; //指针和引用,赋值/初始化,权限可以缩小,但不可以放大 const int c = 2; int& d = c; return 0; }
指针和引用,赋值/初始化,权限可以缩小,但不可以放大
1.3.指针和引用的区别:
从语法角度:引用是不开辟空间的,指针需要开辟空间
从底层角度:两种都是一样的
2.内联函数
2.1.宏的缺点:
1.不能调试
2.没有类型安全的检查
3.有些场景下非常复杂
#define ADD(x,y) ((x)*(y))//正确的宏函数 //宏不是传参,而是替换 #define ADD(x,y) (x)*(y)///((a | b)* (a & b)),错误 #define ADD(x,y) (x*y)///(5+10*6+20),错误 #define ADD(x,y) x*y //5+10*6+20=85,错误,替换可能会造成运算过程出错 int main() { ADD(1, 2); //宏不是传参,而是替换 int a = 1, b = 2; ADD(a| b, a & b); ///(a | b* a & b) return 0; }
这里宏是替换而不是传值,它不会检查替换的值,像(a | b+ a & b)就会错误
使用宏函数,需要尽量加括号,很容易错。
2.2.内联函数
inline int Add(int x, int y) { int z = x + y; return z; } int main() { int ret = Add(1, 2); cout << ret << endl; return 0; }
release中没有call Add,减少了函数调用时栈帧的开辟
效率提高,并且可以调试,很好的替代了宏
inline内联函数是一种以空间换时间的情况,这里的空间指的是编译的指令,不是内存
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; }
这里会出现链接错误:
内联函数不建议定义和实现分离!
当定义和实现分别在.c文件和.h文件中时:程序运行时,当程序运行时,main函数编译到f(10)的地方,会优先编译函数定义并不会链接,而在链接过程中,通过头文件找到函数调用
当f为内联函数时,内联函数是在编译过程完成编译的,因此系统就会认为f()是一个内联函数,所以不会将其链接,所以就会出现链接错误。
3.auto关键字
#include <iostream> using namespace std; int main() { int a = 0; auto b = a; auto c = &a; //typeid(变量名).name()可以获取变量的实际类型 cout << typeid(b).name() << endl;//int cout << typeid(c).name() << endl;//int* return 0; }
3.1auto的好处
std::map<std::string, std::string>dict; std::map<std::string, std::string>dit=dict; //上一行和下一行是一样的,这就是auto的实际好处 auto dit = dict.begin();
3.2typedef的缺点:
typedef char* pstring; int main() { const pstring p1;//编译是否成功 const pstring* p2;//编译是否成功 //p1失败,p2成功 return 0; }
p1:实际上使用typedef后,const pstring p1会变成char* const p1
这里const修饰的是p1,而这样的p1只有一次初始化的机会,因此必须初始化。
4.范围for
自动依次取数组中数据赋值给e对象,自动判断结束
for循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int array[] = { 1,2,3,4,5,6,6,4 }; for (int i = 0; i < sizeof(array) / sizeof(int); i++) { cout << array[i] << " "; } cout << endl; //范围for --语法糖 for (auto e : array) { cout << e << " "; }//两种结果一样 cout << endl;
5.nullptr
void f(int) { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; }//这里函数重载,但结果都是f(int) //C++中,NULL被定义为0,这也不知道为什么是个错误不太好 int main() { f(0); f(NULL); return 0; }
因此,C++11中打了一个补丁,用nullptr来代替NULL。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。