让你不再害怕指针
复杂类型说明
要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样:从变量名处起,根据运算符优先级先后结合,一步一步分析。–(摘自网络经验)
下面我们根据这条原则来做实例分析:
int p; //这是一个普通的整型变量
int *p; //首先从p处开始,先与*结合,所以得到p是一个指针;
//其次再与int结合,可以得出该指针指向一个整型数据;
//所以p是一个返回整型数据的指针。
int p[3]; //(1)首先从p开始,先与[]结合,说明p是一个数组;
//(2)然后与int结合,说明数组元素是整型;
//所以p是一个整型数据组成的数组。
int *p[3]; //(1)p处开始,先与[]结合,得到p是一个数组;
//(2)再与*结合,得到p数组里的元素是指针类型;
//(3)再与int结合,说明指针指向的内容是整型的;
//所以p是一个由返回整型数据的指针所组成的数组。
int (*p)[3]; //(1)p处开始,先与*结合,得到p是一个指针;
//(2)再与[]结合,说明该指针指向一个数组;
//(3)再与int结合,说明数组里的元素是整型的;
//所以p是一个由整型数据组成的数组的指针。
int **p; //(1)从p开始,先与*结合,说明p是一个指针;
//(2)再与*结合,说明指针指向的元素是指针;
//(3)然后与int结合,说明该指针指向一个整型数据;
int p(int); //(1)从p开始,先与()结合,说明p是一个函数;
//(2)然后进入()里分析,说明该函数有个整型变量参数;
//(3)然后再与外面的int结合,说明该函数的返回值是一个整型数据。
int (*p)(int); //(1)从p开始,先与*结合,说明p是一个指针;
//(2)然后与()结合,说明该指针指向一个函数;
//(3)然后与()里的int结合,说明该函数有一个int类型参数;
//(4)再与最外的int结合,说明函数的返回类型是整型。
int *(*p(int))[3]; //(1)从p开始,先与()结合,得到p是一个函数;
//(2)再与()内int结合,说明该函数有一个int型参数;
//(3)再与*结合,说明函数返回的是一个指针;
//(4)然后到最外层,先与[]结合,说明返回的指针指向一个数组;
//(5)然后再与*结合,说明数组里的元素是指针;
//(6)然后再与int结合,说明指针指向的内容是整型数据.
const*只读定义
const
在*的左边:数据就是只读的
const
在*的右边:指针就是只读的 (左数右指)
下面代码来验证:
char a,b,c;
const char *ptr2=&a; //指针可以改变,数据为只读
char * const ptr1=&a; //数据可以改变,指针为只读
*ptr1 = 15;
*ptr1 = 16;
ptr2 = &b;
ptr2 = &c;
结构体指针定义和结构内指针
本小结知识点:
代码段:代码、全局常量、字符串常量
数据段:全局变量、静态变量
BSS段:除了栈以外的所有未初始化变量
堆:动态分配的空间
栈:局部变量(不含静态变量),局部只读变量,局部常量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct tag_student {
int num;
char *name;
};
int main (void)
{
struct tag_student *st1 = NULL;
st1 = (struct tag_student *)malloc(sizeof(struct tag_student));
st1->num = 10;
st1->name = "seafly";//这种方式为字符串常量方式,存储在代码段
//st1->name = (char *)malloc(sizeof(char)*10);
//memcpy(st1->name, "seafly", 6);//这种数组操作型就必须为其动态分配内存
//memcpy()和strcpy()比较:size作为动态分配的重要参数
printf ("num: %d\n", st1->num);
printf ("name: %s\n", st1->name);
return 0;
}
数字菜单实现原理
#include <stdio.h>
int main (void)
{
//char a = 0;
int a = 0;
int flag = 1;
printf ("1.print 1\n");
printf ("2.print 2\n");
printf ("3.print 3\n");
while (flag)
{
printf ("choose: ");
scanf ("%d", &a);
switch (a)
{
case 1:
printf ("111111111111\n");
printf ("111111111111\n");
break;
case 2:
printf ("222222222222\n");
printf ("222222222222\n");
break;
case 3:
printf ("333333333333\n");
printf ("333333333333\n");
break;
default:
printf ("EEEEEEEEEEEE\n");
printf ("EEEEEEEEEEEE\n");
flag = 0;
break;
}
}
return 0;
}
共用体union的用法
由于目前的硬件系统都采用覆盖技术,所以联合体中只有最后被赋值的成员才是有效值。
处理器大端小端的测试:
#include <stdio.h>
union C
{
int i;
char c;
};
int main (void)
{
union C c;
c.i = 1;
if (1 == c.c)
printf ("little endian");
else
printf ("big endian");
printf ("< i=%d, c=%d >\n", c.i, c.c);
return 0;
}
[root@redhat6 Desktop]# gcc test.c
[root@redhat6 Desktop]# ./a.out
little endian < i=1, c=1 >
[root@redhat6 Desktop]#
神总结:因为大小端区别主要是由2个字节的数据,其他字节不用管。
面向过程和面向对象的区别
用面向过程方法求圆面积:
using namespace std;
double r = 0;
double s = 0;
cout << “请输入圆半径: ” << endl;
cin >> r; //cin标准输入,一般为键盘
s = 3.14*r*r;
cout << “圆面积: ” << s << endl;
用面向对象方法求圆面积:
//C语言中抽象一个对象的方法
struct Circle { //抽象圆这个对象
double m_s; //圆的面积
double m_r; //圆的半径
};
//C++中抽象一个对象的方法:
class mycircle { //通过类来抽象圆这个对象
public: //定义该类的属性
double m_s; //圆的面积
double m_s; //圆的面积
public: //定义该类的成员函数
void setr (double r) //成员函数
{
m_r = r;
}
double getr (void)
{
return m_r;
}
void sets (void)
{
m_s = 3.14*m_r*m_r;
return ;
}
double gets(void)
{
return m_s;
}
protected:
private:
};
#include <iostream>
int main (void)
{
using namespace std;
mycircle c1,c2,c3; //用类 定义 变量 对象
double r = 0;
cout << "请输入圆半径:" << endl;
cin >> r; //cin标准输入,一般为键盘
c1.setr(r);//给C1圆的属性赋值
c1.sets();
cout << "圆的面积为:" << c1.gets() << endl;
return 0;
}
//结论1:面向过程加工的是一个一个的函数,面向对象加工的是一个一个的类
//结论2:面向对象类的调用==》不是一步一步执行的。当调用成员函数该函数才执行。
//结论3:类是一个数据类型(固定大小内存块的别名)。
//结论4:定义一个类,是一个抽象的概念,不会给你分配内存,
//结论5:用数据类型定义变量的时候,才会分配内存。
//结论6:class类是一个数据类型,和对象关系是”1:n”
//课后问题抛出:C++编译器是如何处理多个对象,具体一点,就是如何区分是c1,c2,还是c3调用了类中的函数
初学者经典错误-类中不写成员函数
//问题引出:为什么要有成员函数?
//上一节课我们的类使用了成员函数
class circle { //此类没有成员函数
public:
double r;
double pi = 3.1415926;
double s = pi*r*r;
};
#include <iostream>
int main (void)
{
using namespace std;
circle c1;
cin >> c1.r;
cout << c1.s << endl; //运行之后乱码或者运行出错
//因为类中这个在初始化的时候已经执行了,且当时其中的r是一个乱码
//当执行c1.s时只是从其中取值,并没有执行pi*r*r;
}
//结论1:类中增加成员函数的作用:当调用的时候就是执行该运算了。
class circle {
public:
double r;
double pi = 3.1415926;
double s = pi*r*r;
double sets (void) //修改:向该类增加相关成员函数
{
s = pi*r*r;
return s;
}
};
//结论2:类中的成员函数,只有对象在调用的时候才执行成员函数。
//面向过程:树状图类型逐步细化的过程的处理方式
//C语言中的struct只定义了属性, C++中的类把属性和方法做了一个封装
//面向对象:比如界面程序菜单功能,它们一开始就存在,但该功能并没有被执行,只有用户需要点开它的时候它才会执行并处理相关的子任务。
命名空间namespace概念
C中的命名空间:在C语言中只有一个全局作用域,C语言中所有全局标识符共享同一个作用域,当项目过于庞大之后标识符之间很可能容易发生冲突。
C++中的命名空间:命名空间将全局作用域分成不同的部分,不同命名空间的标识符可以同名而不会发生冲突,命名空间可以相互嵌套,全局作用域也叫默认命名空间。
命名空间的定义:namespace name { ... }
使用命名空间:using namespace nameA;
使用命名空间:using namespace nameA:nameB:nameC;
C++命名空间编程实践:
namespace NameSpaceA { int a = 0; }
namespace NameSpaceB {
int a = 1;
namespace NameSpaceC { //命名空间的嵌套
struct Teacher {
char name[10];
int age;
};
}
}
#include <stdio.h>
int main (void)
{
using namespace NameSpaceA;
using namespace NameSpaceB::NameSpaceC;//就可以使用Teacher
printf ("a = %d\n", a);
printf ("a = %d\n", NameSpaceB::a);
Teacher t1 = {"aaa", 30};
printf ("t1.name: %s\n", t1.name);
printf ("t1.age: %d\n", t1.age);
}
//<iostream>文件中没有引用标准的命名空间,也就是该文件中没有这句”using namespace std;”
//结论1:C++标准为了区分C,也为了正确使用命名空间,规定头文件不使用后缀.h
//结论2:C++命名空间定义:namespace name { ...; }
//结论3:C++命名空间使用:using namespace namespaceA;
//结论4:namespace定义可嵌套。
struct关键字类型增强
C语言中的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型;
//C++中的struct是一个新类型的定义声明。
struct Teacher {
char name[32]; int age;
};
//C中使用的时候必须是这样: struct Teacher t1,t2;
//C++中使用时会可以直接定义:Teacher t1,t2;
//struct 和 class关键字完成的功能是一样的,也就是struct中也能用public:等。
C/C++中的三目运算符
int a=10,b=20;
(a < b? a:b)=30; //在C中该表达式返回是一个值:10
//C语言中的三目运算符是一个表达式,表达式不能做左值。
//C语言中表达式的返回值放在寄存器中。
//C语言中表达式的返回值是一个数,所以不能做左值。
//C++中表达式返回的是变量的本身,所以上述代码中在C++中允许使用。
(a < b? a:b)=30; //在C中该表达式返回是一个变量本身:a
//如何让C语言中的三目运算法当左值?
int a=10,b=20;
(a < b? a:b)=30; //在C中该表达式返回是一个值:10
//当左值必须条件:必须是寄存器或内存空间!
//能当左值,其属性应当是一个变量(内存空间)的性质。
//(a < b? a:b)=30;要返回一个内存空间,也就是内存首地址,也就是指针。
//我们不想让它返回a的值,而是返回a的地址,返回b的地址,所以我们的修改如下:
//*(a < b? &a:&b) = 30;//C指针的灵活运用!
C/C++中的const
C中的左数右指
//const 在 * 左边,则作用于该内存空间中的具体数据。
//const 在 * 右边,则作用于指针本身.
const为C中的冒牌货
//C语言中的const定义的常量是一个冒牌常量,因为其可以通过指针间接改变。
//C++中的const定义的常量是一个货真价实的常量,即使通过指针间接也改变不了其值。
//C++中const符号表实现机制:C++编译器扫描到const定义时候,不会像C一样单独给它分配内存,而是把该常量放在一个符号表里,我们不是C++编译器开发者我们也不知道编译器把该常量放到内存四区的哪个区里了,我们只需要知道C++编译器只是对这个进行特别特殊处理。即使我们使用指针取该常量地址,C++编译器会单独给我们这个指针指向的地址单独分配一个内存,所以我们对该内存的修改和那个符号表里的常量没有半毛钱关系。
const和#define的对比
int a = 10;
int b = 20;
int array[a+b] = {0};
//C/C++都不支持,数组下标不能是变量,但Linux内核除外,因为Linux内核的gcc编译器支持该写法。
const int a = 10; //等同于#define a 10
const int b = 20; //等同于#define b 20
int array[a+b] = {0};
//C/C++都支持,编译是可以通过的。
//const用途:在C++中是用来替换#define的一个手段
//const常量是由编译器处理,提供类型检查和作用域检查。
//#define宏定义由预处理处理,单纯的文本替换。
//如何#define作用域限制在某个函数里面?如下:
void func1 (void)
{
#define a 10
const int b = 20;
#undef a
}
普通引用
变量名回顾
变量名实质上是一段连续存储空间的别名,是一个门牌标号。
程序中通过变量来申请并命名内存空间。
通过变量的名字可以使用存储空间。
问题1:对一段连续的内存空间只能取一个别名吗?
引用:就是给一个已定义的变量别名。
引用的语法:Type &name = var;
int a = 10;
int &b = a; //b就是a的别名
b = 100; //相当于把a改成100了。
//引用是C++的语法范畴,所以当你看到引用的时候,你就不能用C的语法来分析程序了。
//普通引用必须奥初始化,也就是必须要依附一个右值。
//引用做函数参数
void swap (int &a, int &b)
{
int c = a;
a = b;
b = c;
c = a;
}
int main (void)
{
int x=10, y=30;
swap (x, y);
}
复杂数据类型如何引用
范例代码:
struct Teacher {
int age;
};
void print1(Teacher *pt) { printf(“age: %d\n”, pt->age);}
void print2(Teacher &pt) { printf(“age: %d\n”, pt.age);}
void print3(Teacher pt) { printf(“age: %d\n”, pt.age);}
int main (void)
{
Teacher t1; t1.age = 35;
print1(&t1); //pt为指针指向t1地址
print2(t1); //pt是t1别名,相当于t1,C++编译器已经帮我们取地址了
print3(t1); //此处是形参,相当于pt = t1
}
引用的本质分析
//引用作为其他变量的别名而存在,因此在一些场合可以代替指针。
//引用相对于指针来说具有更好的可读性和实用性。
//思考:C++编译器为引用背后做了什么呢?
int a = 10;
int &b = a;
//我们上面讨论过,单独定义引用时候,必须初始化,所以它的定义行为很像一个常量。
//回顾引用定义:给一个内存空间取一个别名。
//上面一句验证:读取a,b地址,结果显示两地址相同。
struct Teacher {
int a[10]; //40字节
int b[5]; //20字节
int &c; //4 0 //通过sizeof()的检测,结果为4字节,说明很类似指针。
int &d; //4 0
};
//所以综上结论得出:
//引用在C++中的内部实现是一个常量指针:Type * const name;
//C++编译器在编译中用常指针作为引用的内部实现,因此引用所占空间和指针相同。
//从使用角度,引用会让人误会其只是一个别名,没有自己的存储空间,这是C++为了实用性而做出的细节隐藏。
//当我们使用引用语法时,我们不去关心编译器引用是怎么做的。
//当我们奇怪的语法现象时,我们才去考虑C++编译器是怎么做的。
引用作为函数返回值
int getaa1(void)
{
int a = 10;
return a;
}
//返回a的本身
int &getaa2(void)
{
int a = 10;
return a;
}
int *getaa3(void)
{
int a = 10;
return &a;
}
int main (void)
{
int a1 = getaa1();
int a2 = getaa2(); //接引用方法1:此处它把该函数里面a的副本返回给a2,所以其值就是a2的值。
int &a3 = getaa2(); //接引用方法2:此处它把该函数里面的a地址赋给a3,而该函数运行完毕后就清空释放内存,但内存门牌号还实实在在存在,所以a3指向这个区域的数据是乱码。
}
//C++引用时的难点
//当函数返回值为引用时,若返回栈变量,不能成为其他引用的初始值,不能作为左值使用。
//若返回静态变量或者全局变量,可以成为其他引用的初始值,既可作为右值使用,也可作为左值使用。
//C++链式编程中,经常用到引用,运算符重载专题。