c++基础
一、常见问题
指针 数组
关于指针,实际就是addr,在汇编代码中对应着M[R[xx]];引用类型,只是语法对指针做的一层封装,在传递值的语言以及函数中,拥有对象控制权限,那么传递的就是引用.
1.指针的实质:
对于指针的类型决定了其每次进行移位操作时,内存地址的偏移大小,而任意类型指针其所占的内存大小咋取决于程序和机器内存地址位数
void *类型指针可以代表任意指针类型,作为形参可以接受任意指针,指针之间的互相转换也是非常合理的.
指向指针指针类型如 int**p,从语法角度来理解p是一个指针,其指向了一个int*指针,也就是说该指针内存存放这一个指针的地址 int \*p1=0; int \*\*p=&p1
;
只想指针的引用类型 *&ref
2.数组的实质:
数组表示一段连续的空间,c++很容易将指针和数组概念不能完全分清楚.
1 2 3 4 5 6 7 8 int a[3 ] = { 1 ,2 ,3 }; int *a1= a; int a2 = *a; cout << a << endl ; cout << &a << endl ; cout << a1<<endl ; system("pause" );
汇编代码:
首先[addr]不是间接取址 而是直接取址
1 2 3 4 5 6 7 8 9 10 11 12 int a[3 ] = { 1 ,2 ,3 }; 00 C64CB8 mov dword ptr [ebp-14 h],1 00 C64CBF mov dword ptr [ebp-10 h],2 00 C64CC6 mov dword ptr [ebp-0 Ch],3 int *a1 = a; 00 C64CCD lea eax,[ebp-14 h] 00 C64CD0 mov dword ptr [ebp-20 h],eax int a2 = *a; 00 C64CD3 mov eax,4 00 C64CD8 imul ecx,eax,0 00 C64CDB mov edx,dword ptr [ebp+ecx-14 h] ecx=0 [ebp-14 h]=a00 C64CDF mov dword ptr [ebp-2 Ch],edx
1 2 3 4 5 2. 堆上分配数组int *a=new int [4 ]{1 ,2 ,3 ,4 }
结论:
对于数组如果是局部变量其本身就是通过栈维护的几个连续元素,语法上使用首元素[index]的形式进行访问;
如果将局部数组a符号赋值给数组,那么相当于引用了新的指针变量,指针变量内容为数组元素地址
使用堆数组则表现的如同java语言
3.关于数组和指针在语法上的一些使用:
c++中语法中数组名=指针
既然使用指针来控制数组,那么每次进行的偏移量是根据指针类型确定的,而数组长度则未知,在传递参数时,要告知数组长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void point (int *p) { std ::cout <<"int*p" ; } void pointArrayFun (int (*p)[10 ]) { std ::cout <<p[1 ]-p[0 ]; } void arrayPointFun (int **p) { std ::cout <<p<<std ::endl ; std ::cout <<*p<<std ::endl ; } void pointAndArray () { int a=3 ; int b=3 ; int array [10 ]={1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 }; int * arrayPoint[10 ]={&a,&b}; arrayPointFun(arrayPoint); int (*pointArray)[10 ]=&array ; pointArrayFun(pointArray); }
4.函数指针和函数名
函数名等同于函数指针,也就是说在作为形参,函数名可以匹配函数参数和函数指针参数,无非是在使用的时候是否要使用*符号
当返回复杂类型 使用 auto fun()->return type
作用域和生命周期
c++的作用域有编译单元即文件,函数,函数内部出现的{}符号
全a局符号:
delclear 声明 如外部函数声明,外部变量声明属于弱符号
,对于c++未使用extern关键的外部变量声明会被编译器解释为全局变量这样的强符号
define 全局函数=类函数,全局变量定义都属于强符号
static 修饰静态全局变量,为当前编译单元所有,静态变量可以声明在函数体中
局部变量:
定义在函数体作用域中,编译过程分配在栈上的变量,当作用域结束后随着栈回收而回收
关于类:
c++将类作为了全局符号,类中的函数作为了全局函数,static 类函数只是从编译器的角度来看是静态类函数,而不是和默认static 作为静态单元关键字来使用
1 2 3 4 5 6 7 #ifndef ELFREAD_T_H #define ELFREAD_T_H class T { void test () ; }; #endif
1 2 3 4 5 #include "t.h" #include <iostream> void T::test () { std ::cout <<123 ; }
1 2 3 4 5 6 #ifndef ELFREAD_T2_H #define ELFREAD_T2_H class T { void test () ; }; #endif
1 2 3 4 5 #include "t2.h" #include <iostream> void T::test () { std ::cout <<456 ; }
这两个cpp在链接过程就会出现全局符号重复定义
头文件在c++语言中的作用就是将所谓的声明和定义分开,去看一下链接过程就能完全明白,按照上述定义的符号来理解,头文件的用法就是用来写外部符号,也就是所谓的 declear声明.
1 2 3 CMakeFiles\elfRead.dir/objects.a(test2.cpp.obj): In function `ZN1T4testEv': F:/code/clionProject/elfRead/test2.cpp:6: multiple definition of `T::test()' CMakeFiles\elfRead.dir/objects.a(test.cpp.obj):F:/code/clionProject/elfRead/test.cpp:6: first defined here
这是minGW链接时提示的错误,说明类函数的确是被汇编器当作全局符号的,链接过程在符号解析的过程中抛出错误;
即使此处不适用头文件,直接使用两个cpp,里边写相同的class定义,也会如此.
补充:
对于语法
1 2 3 4 5 6 7 class A {type fun () {} }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> class T {public : void test () {}; void x () ; }; int main () { T t; t.test(); t.x(); } #ifndef ELFREAD_T_H #define ELFREAD_T_H #include <iostream> class T {public : void test () { std ::cout <<123 ; }; void x () ; }; #endif #include <iostream> #include "t.h" void T:: x(){ std ::cout <<"xx" ; }
构造器相关
1.默认构造器
默认构造器不完成任何内容,定义了其他构造器后,编译器不会再创建默认构造器,可以通过default关键字保留默认构造器 XX()=default;
2.构造器初始化列表顺序
1 2 3 4 5 6 7 8 9 10 11 12 class F { int a; int b; public : F(int c,int d):b(c),a(b){ std ::cout <<a<<std ::endl ; std ::cout <<b<<std ::endl ; } }
3.拷贝构造函数和赋值操作符完成了深拷贝工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Person { private : int age; string name; F(const F&f) F&operator =(const F&f) }; void test () {Person p; Person p2=p; Pserson p=p2; }
了解一个事实,编译器对于形参无论是X86还是IA32都创建了一个新的
,无论是任何类型,形参所占用的空间和实参是相同
基本数据类型形参在栈或者寄存器拷贝一份当作形参
指针类型相同,指针本身对于汇编来说也只是一个 值
而已
类类型 分为值传递和引用传递, 前者对于栈对象,如果直接传递对象本身,相当于调用构造器创建新的形参对象;后者指栈对象传递引用或者取地址,都会传递该对象首地址,堆对象和传递引用和指针是相同的.
1 2 3 4 5 6 7 class F { int a; int b; public : F(int c,int d):a(c),d(b){} F(F&f){a=f.a;b=f.b;} }
4.析构函数
会在栈对象销毁后调用,堆对象调用delete过程中free函数调用前调用
析构函数本身{}中由程序员编写,用来释放类中指针成员,指针成员编译器自动释放
如果重写了析构函数,那么相对来说就要重写一个拷贝构造函数和拷贝赋值运算符,因为拷贝对于指针成员来说是浅拷贝,指针会指向同一块,重写的拷贝函数应该完成指针对象构造,具体看3.
指针作为基本数据类型,并不会触发析构函数,所以传递类对象时,传递引用就不会发生上述重写析构导致的内存回收
5.委托构造函数,构造器调用前调用另一个构造完成初始化,以及其中的逻辑代码
1 2 3 4 5 6 7 class F { int a; int b; public : F(int c,int d):a(c),d(b){} F():F(1 ,2 ){} }
6.隐式类型转换可以通过explicit关键字屏蔽
7.仅仅使用数据的类使用struct
8.关于c++ 指针 引用 值 类构造器
首先将指针和引用看作相同的,两者都占用空间,并且其值都是地址,只是c++编译器在语法上对两者进行了区别,属于不同类型,可以参与重载
值传递:参数在汇编指令下会进行一次值复制操作,无论该实参的类型,只是在c++语法层面将这个问题复杂化了
1.关于基本类值传递,就是简单的复制操作;对于简单类型的引用或者指针传递,被称之为引用传递,实际对于引用/指针来说还是存在一次地址值的复制操作,作为寄存器或栈值进行函数调用,只是该参数的行为改动的是原值的
2.对于复杂类型类的值传递问题,对于语法层面的值传递,会导致拷贝构造函数的调用,返回值也是,都存在了创建一份新的对象的操作;当传递指针和引用时候,值传递的是地址值,此时的参数复制工作并不会涉及原对象的拷贝工作
3.关于析构函数,析构函数本身并不做什么工作,对于类成员变量的清除,默认析构函数也不会进行;对于汇编代码的堆栈指针变化造成的所谓回收堆栈
也不会对内存本身数据造成改变,只是在语法上不让代码访问,形参也是如此,不过X86形参会放在寄存器中;这里要说的意思是,只有你的析构函数主动清除了指针成员指向的内存,此时返回临时引用才会造成问题.
二、关键字
const
基础用法:
顶层const 表示 a不能再被赋值 ,底层const 表示不能通过*a进行赋值操作
typedef和using
基础用法:
1 2 3 4 5 typedef typeName type;using typeName=type;using namespace xx;
auto
基础用法:
1 2 3 4 //1.自动推断类型 auto returnFun a; //2.后置返回类型 auto fun(paras)->return Type
decltype
基础用法:
1 2 //获得类型 decltype (expression)
命名空间namespace
基础用法:
1 2 3 4 5 6 7 8 9 10 //1.定义命名空间 namesapce xx{ code } //2.使用命名空间 xx::fun //3.匿名命名空间 namesapce{ } 匿名命名空间和static关键字都可以将该变量 函数的作用域控制在当前文件中
关于匿名命名空间可以参看google c++ style
class struct union
基础用法:
class struct只是默认权限不同,一般仅仅当不含有成员函数的情况使用struct;
union {
int x
double y
} 同时只能一个变量占用内存,也就是说其内存大小= sizeof (int)
/sizeof (double)
三、宏
1.使用宏的保护机制
1 2 3 #ifndef xx #define xx #endif
关于xx的命名方式要体现项目的全路径,如 FOO_BAR_BAZ_H_
关于c++的编译单元是以单个文件进行的,宏保护就是对于单个文件进行的保护
2.使用\表示宏的换行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //测试函数的宏定义 #define Test(Name) \ extern void testFunction(void);\ class TestClass \ { \ public: \ TestClass() \ { \ cout<<#Name; \ testFunction(); \ } \ }instance; \ void testFunction(void) //如何使用 Test(fun) { std::cout<<"test"; }
这个例子还能看出来extern 关系不仅仅在链接时取链接外部函数,还能链接同一个文件中的函数
四、其他特性
1.使用{}进行初始化
1 2 int array[3]={1,2,3} std::vector<int>vec={1,2,3} //google中提倡使用这个替代可变参数
2.关于c++内存管理
编译器只会主动调用栈对象的析构函数,而程序员要自己确定的事情是当该类中含有堆内存的时候要在析构函数中进行delete,delete的作用是调用析构函数,并且释放该指针指向的堆内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Innner {type * var; ~(){ if (var)delete var;} } class F {Inner * in; ~(){ if (in)delete in;} } void test2 (F f) { return f;} void test () {F f; F *f=new F() }
对于栈对象
又因为值传递问题,引出了 拷贝构造函数在 栈对象参数传递 栈对象返回时导致的析构,可能会引起内部指针指向同一处造成的内存问题,如果非要进行值传递,那么就要定义值传递的拷贝构造函数,以及值传递的赋值运算符
关于栈对象中共享堆成员内存,那么就要定义引用传递的拷贝构造函数和赋值运算符,以及析构函数
值行为的类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Class Inner{ public :int a;Inner(int a):a(a){} } class F {Inner* in; F(Inner* in):in(in){} F(const &F f){ this ->in=new Inner(f.in.a); } F& oprator=(const &F f) this ->in=new Inner(f.in.a);} ~F(){ if (in)delete in;}
引用行为的类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Class Inner{ public :int a;Inner(int a):a(a){} } class F {Inner* in; std ::size_t * count;F(Inner* in):in(in),count(1 ){} F(const &F f){ this ->in=f.in; this .count=f.count; this .count++; } F& oprator=(const &F f) count--; clear(); this .in=f.in; this .count=f.count; count++; } ~F(){ count--; clear(); } void clear () { if (count==0 ) delete in; } void test () {}
对于堆对象
还是上述的问题,堆对象句柄c++编译器不会为之进行主动析构,只有程序员主动调用析构函数,理解一下无论时栈还是堆句柄,我们所做的都应该是在其句柄声明周期结束的时候去判断是否应该是否该内存,对于分配在栈上的内存,编译器就是很自信的直接清除了,而令人困惑的就是堆这部分内存.
我的理解是,如果你创建了一个栈对象,并且保持值行为,那么这个对象的内存无论是栈成员,还是指针堆成员,都应该伴随着该句柄的生命而结束;如果定义的是引用行为,那么就要使用引用计数来判定何时释放堆成员.
使用原生指针作为堆句柄时,问题在于你没有办法使用c++本身语法进行引用计数判定
一般来说存在new 就要有与之匹配的delete
堆句柄 1 2 3 4 5 6 7 8 9 Class A{ }; void test () {A *a=NEW A(); A *a2=NEW A(); a=a2; }
智能指针的实质就是通过将指针封装成栈对象,此时编译器对于智能指针这种栈对象就会按照一般情况来处理
3. 左右值
将没有指向句柄
的量称之为右值
如临时变量 1 a 等,
由左值运算出的结果未赋值的情况
string a=“123”;string b=123"; a+b(右值)
引用函数
1 2 3 4 5 6 7 8 9 10 class Temp { void test () & ; void test () && Temp test2 () ; Temp& test3 () ; void test4 () { test2().test() test3().test() } }