站在编译器和C的角度剖析c++原理, 用代码说话
Hello world
首先我们先引入无敌案例之hello world:
#include <iostream>
using namespace std;
int main(void) {
std::cout << "Hello, World!\n";
int i = 0;
cin >> i;
cout<<"你输入的i是"<<i<<endl;
return 0;
}
cout就是表示标准输出终端, 但是这里用了<<来表示,我们知道在C中这表示的是移位运算符,但是这里却表示输出重定向?这就说明c++编译器对这个符号做了增强的功能. 这个话题我们在之后的文章中会讲到,这也是个重点.
面向对象VS面向过程
我们都知道c是面向过程的语言,c++是面向对象的语言, 那么到什么是面向过程什么是面向对象呢,对于初学者来说的确很抽象,看到网上的一些概念更加混乱了,那么我们用代码说话:
//面向过程
int main(void){
double r = 0;
double s = 0;
cout << "请你输入圆的半径";
cin >> r;
s = 3.14 * r * r;
cout << "圆的面积是:" << s << endl;
return 0;
}
//面向对象
struct Circle{
double m_r;
double m_s;
void setR(double r){
m_r = r;
}
double getS(){
m_s = 3.14*m_r*m_r;
return m_s;
}
};
int main(void){
Circle c1;
double r = 0;
cout << "请你输入圆的半径";
cin >> r;
c1.setR(r);
cout << "圆的面积是" << c1.getS() << endl;
return 0;
}
面向过程的代码我就不解释了, 面向对象的这个代码中首先说的是定义Circle这个结构体数据类型,我们知道数据类型的本质是什么?就是固定大小的内存块的名字而已. 所以我们当然可以在c++中继续使用struct, 但是在c++中struct相比于C的变化是能够在结构体中定义方法,能够在里面继承函数, 这也就是封装的概念. 当然在后面更多的是使用class而不是struct.我们先循序渐进的给大家认识本质,让大家看到c++其实不恐怖. c++中的struct和class还有一点不同是struct中定义的方法默认属性是public, class中默认是private的,别急,这些后面都会涉及到,这里只需要知道class很类似struct就行.
struct类型的加强:
C语言的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型.
C++中的struct是一个新类型的定义声明,进行了功能增强,内部还能加函数.
在c++的struct或class中的这些变量m_r
叫做成员变量,void setR(double r){}
这些方法叫做成员方法.
在main中,Circle c1;
就是用类型定义变量,就是分配了个内存的意思. 只有执行c1.setR(r);
c++编译器才会去调用函数.
我们这里介绍个易犯错误模型:
class circle{
public:
double r;
double pi = 3.14;
double area = pi * r * r;
};
int main01_04(void){
circle p1;
cout << "请输入area" << endl;
cin >> p1.r;
cout << p1.area << endl;
return 0;
}
这样的结果是个乱码,为什么呢?是不是你也错了?这个错误知识点上面已经提到过了, 再分析一遍: circle p1;
分配了内存,好,r
因为没有初始化,所以r的内存中是乱码, pi
是3.14, 那么现在area
的内存中就是乱码. 接着将终端输入给r
的内存赋值,好,这时候r
的不是乱码了,但是对area
并无关系,所以还是乱码. 怎么处理呢?当然是像上面一样在里面写方法来调用.
namespace
这里先说一下每次写c++都要打头写#include <iostream>
, 我们知道这是为了包含头文件,但是为啥不写#include iostream.h
呢?这是因为当时设计者为了和C有些大点的区别而做的一些并无卵用的活. 所以没多大意义. 并且当我们使用#include <iostream>
的时候如果没有写下面的using namespace std;
就不能使用好多定义比如cout
,你要想使用就得必须写成std::cout
这种形式,那这到底是为什么呢?那么我我们看看iostream里面到底是什么鬼?
#ifndef _LIBCPP_IOSTREAM
#define _LIBCPP_IOSTREAM
/*
iostream synopsis
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>
namespace std {
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;
} // std
*/
#include <__config>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif
_LIBCPP_BEGIN_NAMESPACE_STD
#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;
_LIBCPP_END_NAMESPACE_STD
#endif // _LIBCPP_IOSTREAM
我们会发现它定义了好多cout, cin等等,但是它本身也没有加using namespace std;
这个语句,也就是说要想它本身定义的cout等用起来,就得配合using namespace std;
. 好,那么我们就说说这个namespace到底是什么鬼?
namespace就是标准作用域, c++标准库的所有定义都被扔在了一个叫std的namespace中. 在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。std是c++标准命名空间,c++标准程序库中的所有标识符都被定义在std中,比如标准库中的类iostream、vector等都定义在该命名空间中,使用时要加上using声明(using namespace std) 或using指示(如std::string、std::vector<int>
).C++命名空间的定义:namespace name { … }.
C++命名空间的使用:
使用整个命名空间:using namespace name;
使用命名空间中的变量:using name::variable;
使用默认命名空间中的变量:::variable;
默认情况下可以直接使用默 认命名空间中的所有标识符
namespace NameSpaceA
{
int a = 0;
}
namespace NameSpaceB
{
int a = 1;
namespace NameSpaceC
{
struct Teacher
{
char name[10];
int age;
};
}
}
int main()
{
using namespace NameSpaceA;
using NameSpaceB::NameSpaceC::Teacher; //::域作用符
printf("a = %d\n", a);
printf("a = %d\n", NameSpaceB::a);
Teacher t1 = {"aaa", 3};
printf("t1.name = %s\n", t1.name);
printf("t1.age = %d\n", t1.age);
system("pause");
return 0;
}
c++之register关键字
register:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。注意是尽可能,不是绝对。你想想,一个CPU 的寄存器也就那么几个或几十个,你要是定义了很多很多register 变量,它累死也可能不能全部把这些变量放入寄存器吧,轮也可能轮不到你.
int main()
{
register int a = 0;
printf("&a = %x\n", &a);
system("pause");
return 0;
}
早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充.
c++中的编译器对register进行了功能增强, 即使没有使用register修饰变量,c++也会自动优化常用的变量.
int main(void){
register int i = 0;
int b = 0;
for (i = 0; i < 10000; i++) {
;
}
printf("%d \n", &i);
return 0;
}
register关键字 请求编译器让变量i直接放在寄存器里面,速度快, 在c语言中 register修饰的变量 不能取地址,但是在c++里面做了内容. register关键字请求”编译器”将局部变量存储于寄存器中, C语言中无法取得register变量地址, 在C++中依然支持register关键字, C++编译器有自己的优化方式,不使用register也可能做优化,会将经常使用的变量自动放在register中. C++中可以取得register变量的地址, 但是C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效。
但是使用register修饰符有几点限制:
首先,register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数.
其次,因为register变量可能不存放在内存中,所以不能用”&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。
早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。
c++之boolean运算
C++在C语言的基本类型系统之上增加了bool, C++中的bool可取的值只有true和false, 理论上bool只占用一个字节,如果多个bool变量定义在一起,可能会各占一个bit,这取决于编译器的实现. true代表真值,编译器内部用1来表示, false代表非真值,编译器内部用0来表示. bool类型只有true(非0)和false(0)两个值. C++编译器会在赋值时将非0值转换为true,0值转换为false.
int main(void){
bool b1;
b1 = 4;
printf("b1 = %d\n", b1);//1
b1 = -1;
printf("b1 = %d\n", b1);//1
b1 = 0;
printf("b1 = %d\n", b1);
int a;
bool b = true;
printf("b = %d, sizeof(b) = %d\n", b, sizeof(b));//1, 1
b = 4;
a = b;
printf("a = %d, b = %d\n", a, b);//1, 1
b = -4;
a = b;
printf("a = %d, b = %d\n", a, b);//1, 1
a = 10;
b = a;
printf("a = %d, b = %d\n", a, b);//10, 1
b = 0;
printf("b = %d\n", b);//0
return 0;
}
c++之三目运算
首先我们考虑C中的三目运算符:
int main()
{
int a = 10;
int b = 20;
(a < b ? a : b )= 30;
printf("a = %d, b = %d\n", a, b);
system("pause");
return 0;
}
这个在C编译器是报错的,三目运算符是一个表达式 ,表达式不可能做左值. (a < b ? a : b )
是一个表达式, 表达式的值在寄存器中,寄存器中是没有地址的,给一个没有地址的存储区赋值肯定gg. 但是在c++中可以, 说明c++编译器进行了功能增强, 返回的是a本身. 元素当左值的必要条件就是,元素有地址空间.那么我们就能这样的改这个代码在C中也能实现左值功能:
*(a < b ? &a : &b) = 30;
, 所以说C++到底干了个啥,其实就是干了这个.
比如说(a < b ? 100 : b)=30
这就错了,100的地址是什么鬼?所以说三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用,说白了就是没地址, 回归之前提到的元素当左值的必要条件就是,元素有地址空间.
总结一下:
C语言中的三目运算符返回的是变量值,不能作为左值使用, C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方,抛砖引出了我们下面讨论以及下一篇文章主要讨论的重点—引用.
c++之const
const在C中其实是个”冒牌货”,让我们开一段C:
int main()
{
const int a = 10;
int *p = (int*)&a;
printf("a===>%d\n", a);
*p = 11;
printf("*p===>%d\n", *p);//11
printf("a===>%d\n", a);//11
return 0;
}
在C中虽然用const定义了a, 也就是说我们不能直接去修改a的值,a变成了只读的, 但是我们通过指针p指向a的地址,然后间接的去修改a的地址指向的内存空间, 那么a的值也就变了.是不是感觉被骗了的不爽. 但是在c++中上面的这段代码就是a返回的依然是10. 这是为什么呢?首先能分析出来的是*p所指的内存空间和&a不一样. 这里解释一下c++编译器对const做了什么手脚:
C语言中的const变量, C语言中const变量是只读变量,有自己的存储空间. 然后在c++中, 一旦用const神明内部就维护了一个符号表, a->10. 这个也是只读的. 编译过程中若发现使用常量则直接以符号表中的值替换, 编译过程中若发现对const使用了extern或者&操作符,则给对应的常量分配存储空间(兼容C), 也就是说C++中的const常量, 并不会给分配空间而是仅仅保存在了符号表中, 只有在&或extern的时候,才会给分配个空间供你随便玩,但是这个空间是不会对之前符号表中的值造成任何影响的. c++中的const修饰的,是一个真正的常量,编译期间就定下来了, 而不是C中变量(只读). 那就有一个问题,这岂不是和宏定义一样了,的确是这样,它们很想,但是也是有不同的:
c++中的const常量类似于宏定义const int c = 5
; ≈ #define c 5
, C++中的const常量在与宏定义不同, const常量是由编译器处理的,提供类型检查和作用域检查, 宏定义由预处理器处理,单纯的文本替换.
void fun1()
{
#define a 10//从这行往下所有的代码都能使用a
const int b = 20;//只有在作用域内能使用
//#undef//但是也是可以卸载掉
}
void fun2()
{
printf("a = %d\n", a);
//printf("b = %d\n", b);
}
const int a = 1;
和int const b = 1;
是一样的.
char buf[20];
getMem(getMem(&buf));
这种情况下C语言是通过的,但是在c++中是不通过的. 本身这个语句就是很危险的, 因为这里其实char **
和char (*)[20]
是不一样的. char (*)[20]
是指向数组的指针, 你一旦写buf = buf + 1;
然后getMem(&buf)
就gg. 所以千万不要将buf地址直接放进去了,一般都是定义一个指针char *p = NULL. 然后再传入p的地址在函数中分配.
还有是区分一些:const Teacher *p
, Teacher *const p
:
Teacher *const p
:表示常指针,表示这个指针只能指向一个地方,也就是p不能再赋值其他的地址.
const Teacher *p
:表示指向常量的指针, 也就是说这个指针指向的常量不能再赋值其他. *p不能再进行赋值.
c++之引用初探
变量名实质上是一段连续存储空间的别名,是一个标号(门牌号), 程序中通过变量来申请并命名内存空间, 通过变量的名字可以使用存储空间. 那么对一段连续的内存空间只能取一个别名吗?在c++中当然不是的.
引用是C++的概念,属于C++编译器对C的扩展.引用基于C的本质就是它是个常指针.
nt main()
{
int a = 0;
int &b = a; //== int * const b = &a
b = 11; //== *b = 11;
return 0;
}
请不要用C的语法考虑, 引用可以看作一个已定义变量的别名, 引用作为函数参数声明时不进行初始化.
nt main()
{
int a = 10;
int &b = a;
//b是a的别名,请问c++编译器后面做了什么工作?
b = 11;
cout<<"b--->"<<a<<endl;
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%d\n", &a);
printf("&b:%d\n", &b);//请思考:对同一内存空间可以取好几个名字吗?
system("pause");
return 0;
}
普通引用在声明时必须用其它的变量进行初始化, 为什么要初始化呢,就是因为上面提到的引用的本质就是常指针,const定义的能不进行初始化吗?
引用作为其它变量的别名而存在,因此在一些场合可以代替指针, 引用相对于指针来说具有更好的可读性和实用性.
int swap1(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
return 0;
}
int swap2(int *a, int *b){
int tmp = *a;
*a = *b;
*b = tmp;
return 0;
}
那么普通的引用有自己独立的空间吗?当然是有的,让我们敲一下这段代码:
struct Teacher {
int &a;
int &b;
};
int main()
{
printf("sizeof(Teacher) %d\n", sizeof(Teacher));//8
return 0;
}
有人在这里会问为什么是8,为什么不需要初始化?不是说需要初始化吗?首先这是常指针是个指针,所以8就知道了吧,初始化的问题,结构体的定义,只能告诉编译器,里面有什么类型的数据,占有多少字节,是不能在里面赋值的。构造函数是类定义里的,在那里面是可赋初值的. 也就是说参数列表,类成员中是可以不初始化话的,在调用函数, 构造对象时才算初始化.
从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏.
C中间接赋值的三个条件:
1. 定义两个变量 (一个实参一个形参)
2. 建立关联 实参取地址传给形参
3. *p形参去间接的修改实参的值.
那么引用就是将后两步c++编译器帮我们手动取了一个实参地址, 传给了形参引用(常指针).
struct Teacher{
char name[64];
int age;
};
//以前我们这样写
void printTe(Teacher *p){
printf("%d\n", p->age);
}
//现在我们用引用了
void printTe(Teacher &t2){//t2就是t1的别名,不存在深浅拷贝问题
printf("%d\n", t2.age);
}
int main(void){
int a = 10;//c编译器分配四个空间的内存,能不能在这块内存给a再取一个别名
//引用的语法:Type& name = var;
int &b = a;//表示b就是a的别名
a = 11;//直接赋值
{
int *p = &a;
*p = 12;//C中的间接赋值
}
b = 14;//这样b和a都是14了
Teacher t1;
Teacher &t2 = t1;
t1.age = 10;
printTe(&t1);
printTe(t1);
}
容我给你分析一下引用的过程: 当Teacher &t2 = t1;
时,进行的内部操作就是Teacher * const t1 = &t1.
, 然后执行printTe(t1);
的时候, 调到了函数参数列表那里,void printTe(Teacher &t2){
C++编译器发现呀?是个引用类型,那我给你转一下: Teacher * const t2. 所以printTe(t1);
这一步就等价于printTe(&t1)
过来了. 然后在函数内部编译器又自动给我们取地址间接改变了值. (注意常指针是可以取改变指针指向的内存空间的值的).
引用一旦生出来,就必须给它赋个值, 就类似于buffer, char buf[20];
一出现就是个常指针(注意与常量指针是不同的).初值就是内存首地址.
联系方式: reyren179@gmail.com