赞
踩
举一个例子说明:你想写一个计算器的小程序可以完成加减乘除四种运算,可以写四个函数,把这四个函数放在一个源文件中,并提供一个头文件(屏蔽了源码细节,同时也保护了自己的知识产权),别人就可以调用它。
接着,你要写一个操作系统的大型程序,而前面写的计算器是操作系统中的一个小工具,同时会有很多像计算器的小工具,你当然可以一个小工具写一个源文件,但是这样源码文件会有很多,更好地做法是,写一个小工具集合源文件,里面写很多小工具类,这样就可以用一个源文件存放很多小工具。而且一个小工具就是一个类很容易区分,符合人的思维习惯,就是为了方便人思考。
析构函数在对象被销毁时调用。
只能使用初始化列表。当然在类内定义的时候也可以赋初值,但是在编码阶段就指定值并不适用所有情景。
为什么常对象只能使用常成员函数?
因为非const成员函数可能会修改成员变量的值。(本质:常对象禁止修改它的成员变量)。
他们都是调用无参构造函数,无区别。
对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数;
对象存在,用别的对象来给它赋值,就是赋值函数。
初始化对象是指,为对象分配内存后第一次向内存中填充数据,这个过程会调用构造函数。只要创建对象,就会调用构造函数。
拷贝构造函数只有一个参数,它的类型是当前类的引用,而且一般都是 const 引用。
1)为什么必须是当前类的引用呢?
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
2)为什么是 const 引用呢?(不管是const对象还是非const对象都可以用来初始化当前对象)
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
如果程序员没有显式地定义拷贝构造函数,那么编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数很简单,就是使用“老对象”的成员变量对“新对象”的成员变量进行一一赋值。但是,对于深拷贝,默认拷贝构造函数就不够用了。
对于基本类型的数据,我们很少会区分「初始化」和「赋值」这两个概念,即使将它们混淆,也不会出现什么错误。但是对于类,它们的区别就非常重要了,因为初始化时会调用构造函数(以拷贝的方式初始化时会调用拷贝构造函数),而赋值时会调用重载过的赋值运算符。对象被创建后必须立即被初始化。
这种将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。
如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针,一般浅拷贝足以。
C++深拷贝和浅拷贝(深复制和浅复制)完全攻略
new和delete是C++里的关键字。malloc()和free()是C语言里的函数。
#include<iostream> #include<stdio.h> #include<stdlib.h> int main() { // new int *p = new int; *p = 5; printf("%d\n",*p); delete p; int *pp = new int[5]; for (int i=0;i<5;i++) { pp[i]=i+1; printf("%d ",pp[i]); } delete[] pp; // malloc // char *pp = (char*)malloc(sizeof(char)*15); // pp[0]='h';pp[1]='e';pp[2]='l';pp[3]='l';pp[4]='o';pp[5]=0; // printf("%s\n",pp); //free // free(pp); return 0; }
类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。
编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。成员变量在堆区或栈区分配内存,成员函数在代码区分配内存。
对象所占用的内存仅仅包含了成员变量。sizeof(对象)
类可以看做是一种复杂的数据类型,也可以使用 sizeof 求得该类型的大小。从运行结果可以看出,在计算类这种类型的大小时,只计算了成员变量的大小,并没有把成员函数也包含在内。
在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。
基类的成员变量排在前面,派生类的排在后面。
用于关联成员函数和成员变量。请见前文C++函数编译原理。
静态成员函数的形参中不会添加this,因此无法访问非静态成员(非静态成员函数也不行,因为该函数内部可能会访问非静态成员,没有this,根本访问不到)。一般通过类来调用。声明和定义与静态成员变量一样。
理解:你给你的朋友授权(声明友元),他可以进你家(访问你的成员变量)。
string可以和C风格字符串混用,这体现了C++兼容C的特点。
C++ 的成员访问权限仅仅是语法层面上的,是指访问权限仅对取成员运算符.和->起作用,而无法防止直接通过指针来访问。
本节的目的不是为了访问到 private、protected 属性的成员变量,这种“花拳绣腿”没有什么现实的意义,本节主要是让大家明白编译器内部的工作原理,以及指针的灵活运用。
通过指针偏移来访问成员。
参考
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
注意点:下面代码中,operator+
函数是complex
类的成员函数(在类的内部),A.m_real
是可以访问到的。
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。
注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。
参考
实际上,就两个方向:升权限(protected 改为 public)和降权限(public 改为 private)。这样做增加了访问权限的灵活性。
虚继承的本质:让某个类做出声明,承诺愿意共享它的基类。
虚继承和普通继承的一个区别:参见3。
1、为了解决多继承时的命名冲突和冗余数据问题(例如菱形继承),C++ 提出了虚继承,使得在派生类中只保留一份间接基类(虚基类)的成员。
在继承方式前面加上 virtual 关键字就是虚继承
2、可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
参考
3、C++ 规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。(D只保留一份数据的话,没必要让B和C构造A了,况且 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。)参考
4、 注意:如果类A1
和类A2
都有成员m_a
,但他俩并不是继承自同一个类,那么继承C
就算虚继承他们,也还是有两份m_a
。
对于普通继承,基类成员变量始终在派生类成员变量的前面,而且不管继承层次有多深,它相对于派生类对象顶部的偏移量是固定的。
而对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。
虚基类表:本质上就是一个数组,存放各个虚基类子对象的偏移地址。参考
构造函数的调用顺序还是和普通继承一样:先调用父类再调用子类。
内存模型则不同:先放指向虚基类表的指针,再放自己的成员变量,最后放虚基类的子对象。
不管是虚基类的直接派生类还是间接派生类,虚基类的子对象始终位于派生类对象的最后面。
包括:指针的赋值,对象的赋值,引用的赋值。
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。
编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。
为了使用基类指针访问派生类的成员函数,引入了虚函数。
在父类和子类的成员函数(子类重写了父类的成员函数)前加上virtual
,就可以实现使用基类指针调用派生类的成员函数。
重写:函数的声明相同,实现不同。
可以只将基类中的函数声明为虚函数。
生物学定义:同一物种中不同形态的个体。(龙生九子,形态各异)
直观理解:想象一个函数,根据传入的参数不同,执行的逻辑和结果也不同。
应用场景:当子类的构造函数中new
了一段内存空间,当使用父类指针指向子类对象,delete
该指针时,只调用了父类的析构函数,而没有调用子类的析构函数,导致内存泄漏。
将父类的析构函数申明为虚函数时,就会先调用子类的析构函数,子类的析构函数又会调用父类的析构函数。
语法:virtual 返回值类型 函数名 (函数参数) = 0;
无函数体,只有声明。
包含纯虚函数的类称为抽象类。(无法创建对象)
问题:为什么编译器能通过指针指向的对象找到虚函数?
答:因为在创建对象的时候,增加了虚函数表。如果一个类包含有虚函数,那么在创建对象的时候就会额外创建一个数组(虚函数表),数组里的每一个元素都是虚函数的入口地址。但是数组和对象是分开存的,对象中有一个指针指向数组的首地址。
语法xxx_cast<newType>(data)
答:C
语言中,强制类型转换很简单:(new type)
。但是这样做有缺点:语义不明确,不利于代码审查,可读性很差。
const int n = 3;
int *p = (int*)&n;
比如上面两行代码,我们从第二行代码根本不知道发生了什么样的类型转换。因此,C++引入了这四种强制类型转换,增强了代码的可读性。
1)static_cast用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。
2)const_cast用于删除变量的const属性,转换后就可以对const变量的值进行修改。
3)dynamic_cast用于在类的继承层次之间进行类型转换,它既允许向上转型(Upcasting),也允许向下转型(Downcasting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助 RTTI 进行检测,所以只有一部分能成功。只能转换指针类型和引用类型参考
4)reinterpret_cast类似C
语言中的强制类型转换,reinterpret
顾名思义,重新解释的意思,即对内存中的数据重新解释。
数据是存在内存中的,数据的类型就是解释数据的方式,数据类型转换就是对内存中的数据重新做出解释。
可以求一个表达式的类型,常用来判断两个类型是否相等。
用法例子。有点像Java中的isInstance()
。
CFather *son = new CSon;
if (typeid(*son) == typeid(CSon))
cout<<"1"<<endl;
Run-Time Type Identification
运行时类型识别。
用父类指针指向子类对象时,有时候编译器在编译期没法确定该指针指向哪个对象,即不能确定*p
的类型。只有程序运行之后,才能确定,这就叫运行时类型识别。参考
底层原理:根据对象指针找到虚函数表的地址,再找到当前类对应的 type_info 对象,就能知道当前对象是哪个类的对象,就能调用相应的被重写的成员函数。
(编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象)
在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。
我们不妨将变量名和函数名统称为符号(Symbol),找到符号对应的地址的过程叫做符号绑定。
编译期就能确定符号对应的地址,就是静态绑定。
等到程序运行的时候才能确定符号对应的地址,就叫动态绑定。
本质上就是函数重载。参考
运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然是错误的。
C++默认参数:实参给形参传值是从左到右依次匹配的。(一旦某个形参有了默认值,那么它后面的形参也必须有默认值)
有了默认参数,按理说就可以少传一些参数,而使用默认值,但是这违背了运算符原本的性质。(比如+号,两边必须有东西)
重载运算符的两种方式:成员函数,全局函数(友元函数)。
c+15.6
(c.operator+(Complex(15.6))
) (c
是complex
类的对象,且该类中存在一个参数的构造函数,所以可以把15.6
转为为complex
对象),不能计算15.6+c
。(15.6).operator+(c)
是错误的,C++ 只会对成员函数的参数进行类型转换,而不会对调用成员函数的对象进行类型转换。double
类型也定义运算符重载呢?如果double
中也重载了operator+
不就可以完成上面不能完成的运算了嘛?C++ 创始人 Bjarne Stroustrup 也曾考虑过为内部类型(bool、int、double 等)定义额外运算符的问题,但后来还是放弃了这种想法,因为 Bjarne Stroustrup 不希望改变现有规则:任何类型(无论是内部类型还是用户自定义类型)都不能在其定义完成以后再增加额外的操作。这里还有另外的一个原因,C内部类型之间的转换已经够肮脏了,决不能再向里面添乱。而通过成员函数为已存在的类型提供混合运算的方式,从本质上看,比我们所采用的全局函数(友元函数)加转换构造函数的方式还要肮脏许多。
我们首先要明白,运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的,是首选的。
C++ 规定,箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。
// 1、在complex类中申明友元函数
// 2、重载输入运算符
istream & operator>>(istream &in, complex &A) {
in>>A.m_real>>A.m_imag;
return in;
}
// 1、在complex类中申明友元函数
// 2、重载输出运算符
ostream & operator<<(ostream &out, complex &A) {
out<<A.m_real<<" + "<<A.m_imag<<"i"<<endl;
return out;
}
C++ 规定,下标运算符[ ]必须以成员函数的形式进行重载。
两种声明格式。
返回值类型 & operator[] (参数); // 既可以访问元素,也可以修改元素
const 返回值类型 & operator[] (参数) const; // 只能访问,不能修改
实际开发中,应该同时提供这两种形式。第二种形式最后的const
修饰整个函数,表示这是一个const
成员函数,const
对象只能调用const
成员函数,所以提供这种形式是有必要的。const
对象表示该对象不可修改,所以返回值应该也为const
。这就是写两个const
的原因。
template
关键字用于定义函数模板,typename
用于声明类型参数(也可以写成class
)。
template<typename T>
void swap1(T &a, T &b) {
T c = a;
a = b;
b = c;
}
注意成员函数也要写模板头。类名后面要写<类型参数1,类型参数2>。
template<typename T1, typename T2> class Point{ public: Point(T1 x, T2 y): m_x(x), m_y(y) {} public: T1 getX() const; void setX(T1 x); T2 getY() const; void setY(T2 y); private: T1 m_x; T2 m_y; }; template<typename T1, typename T2> T1 Point<T1, T2>::getX() const { return m_x; } template<typename T1, typename T2> void Point<T1, T2>::setX(T1 x) { m_x = x; } template<typename T1, typename T2> T2 Point<T1, T2>::getY() const { return m_y; } template<typename T1, typename T2> void Point<T1, T2>::setY(T2 y) { m_y = y; } int main() { Point<int, int> p1(10,20); cout<<"x="<<p1.getX()<<"y="<<p1.getY()<<endl; Point<int, char*> p2(10, "东京180度"); cout<<"x="<<p2.getX()<<"y="<<p2.getY()<<endl; Point<char*, char*> *p3 = new Point<char*, char*>("东京180度", "北纬210度"); cout<<"x="<<p3->getX()<<"y="<<p3->getY()<<endl; return 0; }
C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。
C++输入输出流本质上就是已经定义好的类对象,之所以称它们为"流",C++ 开发者认为数据传输(包含输入和输出)的过程像水一样,从一个地方流到另一个地方,所以称实现输入的为输入流,实现数据输出的为输出流。
C++
项目的文件大致可以分为两类:.h
文件、.cpp
文件。
.h 文件:又称“头文件”,用于存放常量、函数的声明部分、类的声明部分;
.cpp 文件:又称“源文件”,用于存放变量、函数的定义部分,类的实现部分。
这两种文件除了后缀不一样便于区分和管理外,其他的几乎相同。区分两者,并不是C++
语法的规定,而是约定俗称的规范。
1、条件编译。
#ifndef _STUDENT_H
#define _STUDENT_H
class Student {
//......
};
#endif
编译效率低,可移植性好。一般使用这个。
2、使用#pragma once
避免重复引入。
特点:写在文件最开头位置,#pragma once
只能作用于某个具体的文件,而无法向 #ifndef
那样仅作用于指定的一段代码。编译效率高,可移植性差。
3、使用_Pragma
操作符。
将_Pragma("once")
写在文件开头。
综合1和2,兼顾可移植性和编译效率,可以按下面的方式写。当编译器可以识别 #pragma once
时,则整个文件仅被编译一次;反之,即便编译器不识别 #pragma once
指令,此时仍有 #ifndef
在发挥作用。
#pragma once
#ifndef _STUDENT_H
#define _STUDENT_H
class Student {
//......
};
#endif
const
的功能:1、表明其修饰的变量为常量。 2、将所修饰变量的可见范围限制为当前文件。
那么,如何定义 const 常量,才能在其他文件中使用呢?参考
1)将const
常量定义在.h头文件中。
当包含该头文件时,const
常量自然也被包含进来。
.h 头文件的作用就是被其它的 .cpp 包含进去,其本身并不参与编译,但实际上它们的内容会在多个 .cpp 文件中得到编译。
申明可以多次,但定义只能一次,因此头文件中应该只放变量和函数的声明,而不能放它们的定义。但是有三种情况例外,以下三种情况属于定义,但应该放在.h
文件中。
1)头文件中可以定义 const
和static
对象。
解释:因为没有用extern
关键字声明的const
对象仅在当前文件可见,即使被包含进多个文件,也不会出现重复定义的错误。
2)头文件中可以定义内联函数。
解释:内联函数在编译阶段就被展开了(编译器必须在编译时就找到内联函数的完整定义,普通函数都是先声明在链接),放在头文件中刚刚好。
3)头文件中可以定义类。
因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的定义的要求,跟内联函数是基本一样的,即把类的定义放进头文件,在使用到这个类的.cpp文件中去包含这个头文件。
1、vector 容器在申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效。
2、vector 容器还提供了 2 个成员函数,即 front() 和 back(),它们分别返回 vector 容器中第一个和最后一个元素的引用,通过利用这 2 个函数返回的引用,可以访问(甚至修改)容器中的首尾元素。
3、vector的reserve()和resize()的区别:reserve是预留的意思,value.reserve(20);即预留20个元素的空间,capacity变为20,size不变。
value.resize(21)将元素个数改变为 21 个,所以会增加 一些默认初始化的元素。size变为21,capacity也可能改变。
可以看到,仅通过 reserve() 成员函数增加 value 容器的容量,其大小并没有改变;但通过 resize() 成员函数改变 value 容器的大小,它的容量可能会发生改变。另外需要注意的是,通过 resize() 成员函数减少容器的大小(多余的元素会直接被删除),不会影响容器的容量。
4、另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
完全弃
5、emplace_back()(c++11新增)和push_back()的区别
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。
参考
左值:有名字,可以取地址。
右值:无名字,不能取地址。临时对象、字面值。
“在C++之中的变量只有左值与右值两种:其中凡是可以取地址的变量就是左值,而没有名字的临时变量,字面量就是右值”。 正是因为这两种变量分别位于 = 的左右两侧,所以被命名为左值与右值。
左值引用:&
int num = 10;
int &b = num; //正确
int &c = 10; //错误
右值引用:&&
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
为了提高寻址效率。以空间换时间。
将一个数据尽量放在一个步长(CPU的一次寻址)之内,避免跨步长存储,这称为内存对齐。
CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据。64位的处理器也是这个道理,每次读取8个字节。
最后需要说明的是:内存对齐不是C语言的特性,它属于计算机的运行原理,C++、Java、Python等其他编程语言同样也会有内存对齐的问题。
GCC 是由 GUN 组织开发的一个编译器套件,支持很多语言。
gcc是一个通用命令,它会根据不同的参数调用不同的编译器或链接器。也就是说,你可以用该命令编译c++或者别的语言。
为了方便, GCC 又针对不同的语言推出了不同的命令,g++命令用来编译 C++,gcj命令用来编译 Java,gccgo命令用来编译Go语言。
一个中大型软件往往由多名程序员共同开发,会使用大量的变量和函数,不可避免地会出现变量或函数的命名冲突。当所有人的代码都测试通过,没有问题时,将它们结合到一起就有可能会出现命名冲突。
为了解决合作开发时的命名冲突问题,C++ 引入了命名空间(Namespace)的概念。
使用变量、函数时要指明它们所在的命名空间。
很多教程中都是这样做的,将 std 直接声明在所有函数外部,这样虽然使用方便,但在中大型项目开发中是不被推荐的,这样做增加了命名冲突的风险,我推荐在函数内部声明 std。参考
我们知道,C语言代码是由上到下依次执行的,不管是变量还是函数,原则上都要先定义再使用,否则就会报错。但在实际开发中,经常会在函数或变量定义之前就使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个变量或函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
例如,我们知道使用 printf()、puts()、scanf()、getchar() 等函数要引入 stdio.h 这个头文件,很多初学者认为 stdio.h 中包含了函数定义(也就是函数体),只要有了头文件程序就能运行。其实不然,头文件中包含的都是函数声明,而不是函数定义,函数定义都在系统库中,只有头文件没有系统库在链接时就会报错,程序根本不能运行。
extern 是“外部”的意思,很多教材讲到,extern 用来声明一个外部(其他文件中)的变量或函数,也就是说,变量或函数的定义在其他文件中。不过我认为这样讲不妥,因为除了定义在外部,定义在当前文件中也是正确的。例如,将 module.c 中的int m = 100;移动到 main.c 中的任意位置都是可以的。所以我认为,extern 是用来声明的,不管具体的定义是在当前文件内部还是外部,都是正确的。
参考
一同:函数名必须相同。
一不同:参数列表必须不同。
(返回值无要求)
在C++代码中调用C语言函数,由于编译方式的不同,函数重命名方式不同,导致找不到调用的函数实现。因此加上extern “C” 关键字,来告诉编译器用处理C语言代码的方式处理C++代码。参考
是为了检查声明和定义的一致性,只有声明和定义在一个文件中,编译器才会进行一致性检查,否则有可能在运行的时候出现不好排查的bug。参考
引用是对指针的封装(引用占用的内存和指针占用的内存长度一样)。让代码书写变简洁。
n维数组名是指向n-1维数组的数组指针,所以sort1()
的第一个形参的类型应该是一个指向字符数组的指针(该字符数组大小为80)。
#include<stdio.h> #include<string.h> void sort1(char (*word)[80], int n) { char p[80] = {0}; int i, j; for (i = 0; i < n-1; i++) { for (j = 0; j < n-1-i; j++) { if (strcmp(word[j], word[j+1])>0) { strcpy(p,word[j]); strcpy(word[j], word[j+1]); strcpy(word[j+1], p); } } } } int main() { srand(time(0)); // n维数组名是指向n-1维数组的数组指针 char a[][80] = {"bak", "anxj", "kja", "avhs"}; sort1(a, sizeof(a)/sizeof(char[80])); int i; for (i = 0; i < 4; i++) printf("%s\n", a[i]); return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。