站在编译器和C的角度剖析c++原理, 用代码说话
引用深入
引入简单的介绍请参考引用初探这篇文章. 然后我们先进行普通引用的深入. 那什么是普通引用呢?就是基本数据类型的引用,而不是结构体或者类的引用,这个话题我们会单独专题讨论,这块儿是比较复杂的. 回归正题. 先上代码:
int getAA1()
{
int a;
a = 10;
return a;//基础类型返回的时候也会有一个小的副本.
}
int & getAA2()
{
int a;
a = 10;
return a;
}
int main(int argc, const char * argv[]) {
int a1 = 0;
int a2 = 0;
a1 = getAA1();
a2 = getAA2();
int &a3 = getAA2();
printf("a1%d\n", a1);//10
printf("a2%d\n", a2);//10
printf("a3%d\n", a3);//乱码
return 0;
}
首先分析这个易犯错误模型. 咱们先讨论a3为什么会乱码?一个函数返回不管是一个引用还是常量,如果不是静态变量或全局变量,那么出了作用域就会释放掉内存空间.这种说法其实少了一个阶段. 当return后, 首先编译器会将这个值产生一个副本存放在寄存器或者内存其他地方,然后作用域内的有明确地址的内存释放,但是小副本还没释放,当一旦执行赋值操作后,这个副本才会被释放. 是不是对a3的乱码有点眉目了. 回顾上一篇说到引用的本质, 那么当函数返回引用的时候,其实返回的就是地址. 因为你用int &a3 =
这种方式接的时候其实编译器也是偷偷的取了地址. 那么,返回出这个地址给了a3接上,这时这个地址指向的内存空间就释放了,但是当我们调用Printf
的时候,隐藏做了个*p
的操作, 这时不乱码就怪了!!!那么为什么a2会正确呢?是因为返回引用后直接用一个变量接到了. 这里有些同学就会疑惑:what?难道还能用一个不是引用的变量去接引用?是可以的. a2 = getAA2();
这里返回的是引用,但是a2却不是引用类型,那么赋值的时候进行了的是引用值拷贝操作,他们俩的地址肯定是不同的,只是将地址指向的内存空间的值进行了拷贝,这样的话回归我们的话题,将一个值已经存在了变量中了,那你随便释放祖先都无所谓啊. 然后a1的问题呢,大家凭直觉都知道是对的,但是自行深层次用上面的方法考虑一下吧?
接下来再考虑一种情况:
int j(){
static int a = 10;
a++;
printf("a%d\n", a);
return a;
}
int & j1(){
static int a = 10;
a++;
printf("a%d\n", a);//101
return a;
}
int main(){
j();//11
j();//12
j1();//11
j1();//12
//j() = 100;会报错,常量值没有内存不能当左值
j1() = 100;
return 0;
}
首先a放在了静态区,所以它的地址并不会随着作用域的影响而析构. 具体到j() = 100;
的问题我们自然清楚了吧,返回的是个常量并没有内存空间,所以是没法当左值的. 但是j1() = 100;
返回的是本质就是地址,所以OK. 当引用当左值的时候,编译器一看是个引用,机会自动*p
操作. 结论就是当被调用的函数当左值的时候必须返回一个引用.
我们再看一个案例:
int g1(int *p){
*p = 100;
return *p;
}
int & g2(int *p){
*p = 100;
return *p;
}
int main(void){
int a1 = 10;
a1 = g1(&a1);
int &a2 = g2(&a1);
printf("a1%d\n", a1);//100
printf("a2%d\n", a2);//100
return 0;
}
a1 = g1(&a1);
是典型的C中间接赋值操作,就不说了. int &a2 = g2(&a1);
这种方式就不会乱码的原因是,内存已经提前打造好了,虽然返回的是引用,但是其实返回的就是打造好的这块地址. 所以没有被析构掉.
当我们使用引用的语法的时候,我们不去关心编译器是怎么做的,当我们分析乱码问题的时候,我们才去考虑编译器怎么用的. 我们必须思考问题的时候跳进去思考,一开始很多人都会觉得这样太繁琐了,很简单的事想的那么复杂了,但是解决问题能力就是从这种方式提高的.
在C中我们经常使用二级指针来进行间接分配内存地址空间, 那么我们看这样一段代码:
struct Teacher
{
char name[64];
int age;
};
int getTe(Teacher **myp){
Teacher *p = (Teacher *)malloc(sizeof(Teacher));
if(p == NULL){
return -1;
}
memset(p, 0, sizeof(Teacher));
*myp = p;
return 0;
}
int getTe2(Teacher* &myp){//指针的引用
myp = (Teacher *)malloc(sizeof(Teacher));
myp->age = 34;
return 0;
}
int main(void){
Teacher *p = NULL;
getTe(&p);
printf("%d", p->age);
getTe2(p);
return 0;
}
getTe(&p);
就是我们C中的方式,这里不做讨论. 首先不要被getTe2中的形参Teacher* &myp
吓住,这就是指针的引用,我们之前一种用的都是变量的引用,其实一样的,就是传入的值不能是一个变量而是一个指针了. myp是p的别名,所以当我们操作myp的时候就相当于是给p分配内存空间了,但是p的地址已经是有了的. 这样就是不用二级指针同样打造了二级指针的效果. 具体过程就是编译器发现是个引用,然后就对p取地址然后扔进函数中,&p
不就是二级指针了吗?为什么耳机指针分配内存就能不受作用域影响?好问题,我也是刚思考到这个问题,这样,当定义一个指针指向NULL的时候,这个4字节的内存区有个地址,但是这个栈中的这个内存中的内容时NULL, 如果不用二级指针的话传入的参数是一个空指针,对空指针的操作是能够使程序崩溃的. 所以用二级指针,使用malloc后,是在堆上分配了内存,并且地址号就存在了p的内存空间中了,这样*p找到了malloc的分配的内存中值. 那出了作用域岂不是内存释放了?这样p的内存空间中的内容不就是指向乐意个可能不存在的地方?堆上的内存你不free是永远不释放的.
我们再讨论一下常引用问题. 在C++中可以声明const引用, const Type& name = var;
, const引用让变量拥有只读属性.
int main()
{
int a = 10;
const int &b = a;
//int *p = (int *)&b;
b = 11; //err
//*p = 11; //只能用指针来改变了
cout<<"b--->"<<a<<endl;
printf("a:%d\n", a);//11
printf("b:%d\n", b);//11
printf("&a:%d\n", &a);//-272632296
printf("&b:%d\n", &b);//-272632296
system("pause");
return 0;
}
感觉上一篇中C和C++之const没有讲清楚,这里再强调一下:
int main(){
const int a = 2;
int* p = (int*)(&a);
*p = 30;
cout<<&a<<endl; //0x28ff08
cout<<p<<endl; //0x28ff08
cout<<a<<endl;//3
cout<<*p<<endl;//20
}
++中用const定义了一个常量后,不会分配一个空间给它,而是将其写入符号表(symbol table),这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。但是const定义的常量本质上也是一个变量,是变量就会有地址,那么什么时候会分配内存?
我们看到,通过 int*p = (int*)(&a);这种方法,可以直接修改const常量对应的内存空间中的值,但是这种修改不会影响到常量本身的值,因为用到a的时候,编译器根本不会去进行内存空间的读取。这就是c++的常量折叠(constant folding),即将const常量放在符号表中,而并不给其分配内存。编译器直接进行替换优化。除非需要用到a的存储空间的时候,编译器迫不得已才会分配一个空间给a,但之后a的值仍旧从符号表中读取,不管a的存储空间中的值如何变化,都不会对常量a产生影响。
但是常引用是有不同的,当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名,使用常量对const引用初始化后将生成一个只读变量. 也就是说两个在const int &b = a;
的时候就都有了地址,并且地址相同. 这可是不同于普通类型的常量的.
inline内联函数
inline关键字必须和函数实现放在一块儿,内联是一个请求,告诉编译器进行内联编译. 什么是内联编译:会直接将函数体插入到函数调用的地方. 是不是又想到了宏? 宏代码片段是由预处理器处理,进行简单的文本替换,没有任何编译的过程.内联函数是由编译器处理,直接将编译后的函数体插入到调用的地方.当然现代编译器进行了优化,在我们没有写inline声明的时候,编译器进行了内联编译.
写内联函数注意事项:
在内联函数中不能存在任何形式的循环语句, 不能存在过多的条件判断语句, 函数体不能过于庞大, 不能对函数进行取地址操作, 函数内联声明必须在调用语句之前.
内联函数相对于普通函数的优势只是省去了函数调用时的压栈,跳转和返回的开销.因此,当函数体的执行开销大于压栈,跳转和返回所用的开销时,那么内联函数将无意义.
结论:
1. 内联函数在编译时直接将函数体插入函数调用的地方。
2. inline只是一种请求,编译器不一定允许这种操作
3. 内联函数省去了普通函数调用是压栈,跳转和返回。
#define MYFUNC(a, b) ((a) < (b) ? (a) : (b))
inline int myfunc(int a, int b)
{
return a < b ? a : b;
}
int main()
{
int a = 1;
int b = 3;
//int c = myfunc(++a, b);
//带参数的宏和普通函数区别
int c = MYFUNC(++a, b); //((++a) < (b) ? (++a) : (b))
printf("a = %d\n", a); //2 //3
printf("b = %d\n", b); //3 //3
printf("c = %d\n", c); //2 //3
printf("Press enter to continue ...");
system("pause");
return 0;
}
默认参数和占位参数
这些概念都很简单,我们直接用代码进行分析就行:
//默认参数
void printAB(int x = 3)
{
printf("x:%d\n", x);
}
//在默认参数规则 ,如果默认参数出现,那么右边的都必须有默认参数
void printABC(int a, int b, int x = 3, int y=4, int z = 5)
{
printf("x:%d\n", x);
}
//占位参数,主要是为了兼容旧的系统
int func (int x, int y, int )
{
return x + y;
}
int func2(int a, int b, int = 0)
{
return a + b;
}
int main_04()
{
printAB(2);
printAB();
// func(1, 2);两个还调不起来
//func(1, 2, 3);
//如果默认参数和占位参数在一起,都能调用起来。。。。
func2(1, 2);
func2(1, 2, 3);
return 0;
}
函数重载
编译器调用重载函数的原则:
1. 将所有同名函数作为候选者
2. 尝试寻找可行的候选函数
3. 精确匹配实参
4. 通过默认参数能够匹配实参
5. 通过默认类型转换匹配实参
6. 匹配失败
//函数名称一样,但是参数数量不一样,类型不一样。。参数的顺序也不一样。。。
//函数重载。。
//函数返回值,不是判断函数重载的标准。。。
void myprint(int a)
{
printf("a:%d \n", a);
}
void myprint(int a, char *p)
{
printf("a:%d \n", a);
}
void myprint(char *p, int a)
{
printf("a:%d \n", a);
}
void myprint(double a)
{
printf("a:%d \n", a);
}
void main()
{
myprint(10);
myprint(10,"ddddd");
myprint("ddd",11);
system("pause");
}
当函数重载遇上默认函数参数 出现二义性问题:
int func(int a, int b , int c= 0)
{
printf("a:%d ", a);
return 0;
}
int func(int a, int b)
{
printf("a:%d ", a);
return 0;
}
void main()
{
int c = 0;
//存在二义性,调用失败,编译不能通过
//c = func(1, 2); //二义性,编译器区分不出来调用哪个。。。。
c = func(1, 2, 3); //这里就没有二义性了
}
函数类型定义
首先我们做几个区分: 定义一个数组类型typedef int MYTYPEAarry[10];
之后就可以这样使用MYTYPEAarry a1;
, 就相当于int a[10];
.
定义一个数组类型指针类型,typedef int (*MYArrayP)[10];
, 表示指向数组的指针, 可以这样使用MYArrayP myarray = &a1;
. int (*myP)[10]; //告诉编译器给我分配4个字节的内存。。。我要做一个指针变量 这个变量指向一个数组.
int func(int x) // int(int a)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
//定义一个类型,,函数类型。。
//这个函数是 int aaa(int a);
typedef int(*PFUNC)(int a); // int(int a)
typedef int(*PFUNC2)(const char *p); // int(int a)
int main(int argc, char *argv[])
{
int c = 0;
//func是一个函数名,函数名就代表函数的入口地址,函数名就是函数指针变量
{
PFUNC p = func;
c = p(1);
}
//c = p("ddddd");
printf("c = %d\n", c);
{
PFUNC2 myp2 = func;
myp2("aaaa");
}
return 0;
}
类的封装
类的封装:
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.
类通常分为以下两个部分:类的实现细节和类的使用方法.
封装的基本概念:一些类的属性是对外公开的,一些类的属性是需要保密的,因此,需要在类的表示法中定义属性和行为的公开级别。类似文件系统中文件的限制.
C++中类的封装:
成员变量:
c++中用于表示类属性的变量
成员函数:
c++中用于表示类行为的函数
在c++中可以给成员变量和成员函数定义访问级别:
public:
成员变量和成员函数可以在类的内部和外部访问和调用.
private:
成员变量和成员函数只能在类的内部进行访问和调用
class Cube
{
public:
int getA()
{
return m_a;
}
int getB()
{
return m_b;
}
int getC()
{
return m_c;
}
void setABC(int a=0, int b = 0,int c=0)
{
m_a = a;
m_b = b;
m_c = c;
}
void setA(int a)
{
m_a = a;
}
void setB(int b)
{
m_b = b;
}
void setC(int c)
{
m_c = c;
}
public:
int getV()
{
m_v = m_a*m_b*m_c;
return m_v;
}
int getS()
{
m_s = 2*(m_a*m_b + m_b*m_c + m_a*m_c);
return m_s;
}
protected:
private:
int m_a;
int m_b;
int m_c;
int m_v;
int m_s;
};
int main()
{
Cube c1;
c1.setA(1);
c1.setB(2);
c1.setC(3);
cout<<"s是:"<<c1.getS();
Cube c2;
c2.setABC(1, 2, 3); //默认参数
cout<<"s是:"<<c2.getS();
return 0;
}
class Point
{
public:
void setP(int _x0, int _y0)
{
x0 = _x0;
y0 = _y0;
}
int getX()
{
return x0;
}
int getY()
{
return y0;
}
private:
//定义圆心和圆的半径
int x0;
int y0;
};
class AdvCircle
{
public:
void setCircle(int _x1, int _y1, int _r)
{
x1 = _x1;
y1 = _y1;
r = _r;
}
void judge(int x0, int y0)
{
int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
if (a > 0)
{
cout<<"点在圆外";
}
else
{
cout<<"点在圆内";
}
}
//类做函数参数的时候,类封装了属性和方法,在被调用函数里面, 不但可以使用属性,而且可以使用方法(成员函数);
//这也是面向对象和面向过程的一个重要区别。。。。
void judge(Point &p)
{
int a = (x1-p.getX())*(x1-p.getX()) + (y1-p.getY())*(y1-p.getY()) - r*r;
if (a > 0)
{
cout<<"点在圆外";
}
else
{
cout<<"点在圆内";
}
}
private:
//定义圆心和圆的半径
int x1;
int y1;
int r;
};
int main()
{
Point myp;
AdvCircle c1;
myp.setP(1, 1);
c1.setCircle(2, 2, 3);
//c1.judge(myp);
c1.judge(1, 1);
system("pause");
}
void main111()
{
//定义点
int x0 = 1;
int y0 = 1;
//定义圆心和圆的半径
int x1 = 2;
int y1 = 2;
int r = 3;
int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
if (a > 0)
{
cout<<"点在圆外";
}
else
{
cout<<"点在圆内";
}
system("pause");
}
.hpp和.cpp
我们需要将我们的代码进行.h和实现进行分离了:
MyPoint.hpp:
#pragma once
class MyPoint
{
public:
void setP(int _x0, int _y0);
int getX();
int getY();
private:
//定义圆心和圆的半径
int x0;
int y0;
};
MyCircle.hpp:
#pragma once//表示这个头文件只能加载一次,类似于C中头文件一开始的`ifdef`
#include "MyPoint.hpp"
class MyCircle
{
public:
void setCircle(int _x1, int _y1, int _r);
void judge(int x0, int y0);
//类做函数参数的时候,类封装了属性和方法,在被调用函数里面, 不但可以使用属性,而且可以使用方法(成员函数);
//这也是面向对象和面向过程的一个重要区别。。。。
void judge(MyPoint &p);
private:
//定义圆心和圆的半径
int x1;
int y1;
int r;
};
MyPoint.cpp:
#include "iostream"
#include "MyPoint.hpp"
using namespace std;
void MyPoint::setP(int _x0, int _y0)
{
x0 = _x0;
y0 = _y0;
}
int MyPoint::getX()
{
return x0;
}
int MyPoint::getY()
{
return y0;
}
MyCircle.cpp:
#include "iostream"
#include "MyCircle.hpp"
using namespace std;
void MyCircle::setCircle(int _x1, int _y1, int _r)
{//运用域作用符号,这个括号就相当于在类的内部了
x1 = _x1;
y1 = _y1;
r = _r;
}
void MyCircle::judge(int x0, int y0)
{
int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
if (a > 0)
{
cout<<"点在圆外";
}
else
{
cout<<"点在圆内";
}
}
void MyCircle::judge(MyPoint &p)
{
int a = (x1-p.getX())*(x1-p.getX()) + (y1-p.getY())*(y1-p.getY()) - r*r;
if (a > 0)
{
cout<<"点在圆外";
}
else
{
cout<<"点在圆内";
}
}
构造函数初探
在我们之前的代码的类中我们都是显示的对成员变量进行初始化操作, 这是不好的,因为如果要初始化一个很多对象的时候,就过于浪费生命了. 所以C++的类中引入了构造方法,
class Test
{
public:
//构造函数 无参构造函数 默认构造函数
Test()
{
a = 10;
}
//带参数的构造函数
//调用方法3种
Test(int mya)
{
a = mya;
}
//第三中初始化对象的方法
//赋值构造函数 copy构造函数
//copy构造函数的用法 4种应用场景,因为涉及到拷贝构造函数,所以这次先不考虑这种
Test(const Test & obj)
{
;
}
protected:
private:
int a;
};
int main()
{
//1 ()
Test t1(10); //c++默认调用有参构造函数 自动调用
// =
Test t2 = 11; //c++默认调用有参构造函数自动调用
//手工调
Test t3 = Test(12); //我们程序员手动调用构造函数
return 0;
}
联系方式: reyren179@gmail.com