位运算包括与运算(and &),或运算(or |), 取反运算(~),左移运算(<<),右移运算(>>)。运算规则不多说,主要看看位运算的应用。
1. 乘除法运算。
利用左移可以实现2次幂乘法运算,比如比如n * 8, 则 n << 3,而n * 9 ,可以写成 n + (n << 3);除法运算同理。
2、加减法运算。
先做无进位加法,即1 + 1 = [1]0, 进位去丢掉,即异或运算。然后加上进位,显然只有1 + 1才会产生进位10, 我们可以写成 (1 & 1 ) << 1即进位可以先做与运算再左移以为得到,根据这样的思路,c代码为:
int add(int a, int b) { if (a == 0) return b; if (b == 0) return a; int t = a ^ b; int c = (a & b) << 1; return add(t, c); } int sub(int a, int b) { return add(a, add(~b, 1)); }
3、交换两个数。
我们知道x ^ x == 0, a ^ x ^ x == a;根据这个思路交换两个数为:
void swap(int &a, int &b) { a ^= b; /* 此时a = a ^ b */ b ^= a; /* b = b ^ a ^ b , 相当于 b = a; */ a ^= b; /* a = a ^ a ^ b ,此时a = b */ }
4、把一个数的最低位1置0.
即1110变为1100, 1111变为1110,1000变为0000等。常规做法是找到最低位1,即不断用1左移做位与运算看看是否等于1,然后再设置该位为0。更简单的方法是直接与n-1做位与运算,即
n &= n - 1
5、计算一个数1的个数。
根据4,每次把最低位1设为0, 直到等于0.即
int count(int n) { int c = 0; while (n) { c++; n &= (n - 1); } return c; }
6、 判断奇偶性。
bool is_odd(int n) { return n & 0x1; }
7、奇偶校验。
8、若一个数组中除了一个重复奇数次外,其他数字均重复偶数次,求那个重复奇数次数的数。
如1, 1, 2, 2, 3则返回3。思路还是x ^x ^ x ... ^x,当有偶数个x时,结果为0, 奇数次数则为x,因此把数组所有元素异或起来,偶数次的必然抵掉掉,剩下一个奇数次的。
int oddNumber(int *a, int n) { assert(a != NULL); assert(n > 0); int t = 0, i; for (i = 0; i < n; i++) t ^= a[i]; return t; }
9、获取整型最大值
#define INT_MAX (((unsigned int) -1) >> 1) #define LONG_MAX (((unsigned long) -1) >> 1) #define LLONG_MAX (((unsigned long long) -1) >> 1)
10、判断一个数是否2的幂,只要是2的幂,则这个数至多只有一个1。
bool is_pow_2(int n) { return (n & (n - 1)) == 0; }
11、另外还有设置某位,获取某位等,根据常识,不多解释。
还有更多关于位运算的应用技巧,以后想到或者看到,再补充。
STL中定义了三种顺序容器类型,分别是vector, list, deque(双向队列),它们都是线性表,区别在于操作和作用于各种操作的开销上。另外STL还提供了三种容器适配器,分别是stack,queue,priority_queue,它们都是deque的重新封装。下面将分别介绍三种顺序容器,包括它们的性能分析和实现方式。注意本文不是讲解容器的使用方法,具体使用方法可以,可以google或者查看文档。
首先是vector,vector的实现方式是动态数组。显然既然是数组,那所有元素必然是连续存储的。vector主要靠不断维护三个指针(迭代器,vector中的迭代器本质就是指针,参考我的另一篇博客:浅谈cpp中vector中的迭代器)来工作的,它们分别是:_M_start, _M_finish, _M_end_of_storage 。_M_start指向第一个元素,_M_finish指向最后一个元素的下一个位置,即哨兵。_M_end_of_storage指向当前已分配地址的最后一个单元。其他两个都容易理解,主要看看_M_end_of_storage,为什么会有这个指针呢?这是因为vector为了减少分配内存的次数,会预先分配比已容纳元素更大的空间。在插入元素时,如果当前不够空间,则会重新分配内存,一般为原来空间的2倍关系分配。注意由于vector必须是连续存储,当重新分配内存时,就会破坏原来的连续性(谁能够保证一定在原来的位置的末端分配呢?),因此重新分配内存时开销是巨大的,首先需要分配足够的内存,然后要把原来的元素逐一拷贝到新分配的内存上,然后还需要释放掉原来已经不需要的内存。如果存储的元素都是基本数据类型还好点,如果是类,则无论是类的拷贝还是调用析构函数释放内存,开销都可能很大。因此为了提高性能,vector总是尽一切可能减少内存分配次数,而同时又要兼顾不能过多分配内存造成空间浪费。注意这三个指针均是动态变化的,会随着容器的变化而自动更新,当调用begin()方法时,其实返回的正是_M_start,而end()方法返回的是_M_finish,当计算capacity时,就需要用到_M_end_of_storage。
下面讨论vector的性能。首先访问,由于本质是数组,而数组本身是支持随机访问的,因此访问元素开销很小,O(1)的时间完成,比如返回v[n],则只需要返回 *(begin() + n)即可。再看看插入元素,如果在尾部追加元素,如果已分配的内存还足够大,则只需在尾部追加即可,即*_M_finish++ = new_item; 如果_M_finish已移到_M_end_of_storage,则表示已分配内存不足,需要重新分配,这个开销很大,前面已经解释。注意,虽然在尾部追加元素时遇到分配内存时开销巨大,但这种情况往往很少发生,因为大多数情况,内存都是足够的。如果需要在最前面添加元素,则不仅需要考虑内存问题,还需要移动元素,这个开销更大,因此vector不直接提供push_front方法,如果确实需要这样的操作,可以使用insert方法实现。但使用之前,一定要想想,真的有这个必要吗?你考虑了其中的开销吗?如果有100W个元素,需要移动100W次,你考虑过这样的成本吗?有没有其他的容器可供选择呢?删除元素和插入元素类似,只是不用考虑内存问题,当移除最后一个元素时,只需移动_M_finish指针并释放掉删除的元素即可(调用元素的析构函数),而移除中间元素,需要移动元素,O(n)的时间复杂度)。
因此总结下,vector是连续存储的,适合随机访问,适合在尾部增删操作元素(这种情况,性能甚至优于list,因为list追加元素每次都需要分配一个元素节点的内存,而vector预先分配好了),但不适合在中间进行增删操作。
接下来讨论list,list实现方式是环形双向链表,学过数据结构的一定非常熟悉了,每个节点不仅保存着元素(data),还分别有两个指针(prev,next)分别指向前一个节点和后一个节点,而最后一个节点的next指向第一个节点,第一个节点的prev指向最后一个节点。节点每次都是动态分配的,即每增加一个节点,需要new一个。存储方式显然不是连续的。节点定义如下:
struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_prev; }; template <class _Tp> struct _List_node : public _List_node_base { _Tp _M_data; };
不难看出,节点和我们数据结构上的节点定义类似,只不过_List_node_base没有data,然后_List_node继承并添加了data,这样的目的是为了使移动和迭代器分离。迭代器不再是vector中简单的指针,而是一个相对复杂的struct,不过我们仍然可以看作是指向节点指针的指针。由于本质是一个双向链表,因此只需要一个指针就可以访问所有元素,list中是_M_node,它指向最后一个节点。那第一个节点就是_M_node->_M_next。因此begin()方法返回_M_node->_M_next, end()方法返回_M_node,当_M_node->_M_next == _M_node时,即_M_node指向自己时,说明链表为空。list正是通过不断维护_M_node这个节点指针来进行各种操作的。另外还有两个指针,分别是_M_put_node 和_M_put_get,这两个指针是临时指针,主要用于新建指针或删除指针时使用。原理就是这样,下面讨论性能:
首先是访问节点,访问最后一个节点或者第一个节点不需要移动指针,即最后一个节点返回_M_node -> _M_data即可,而第一个节点,只需要返回_M_node_->_M_next->_M_data,因此开销很小。但要访问中间节点时就必须通过移动节点了,至于向前还是向后移动,取决于节点的位置。因此list访问节点的时间复杂度是O(n),相对于vector来说性能下降,因此list没有实现[ ]运算符重载。而插入和删除指定位置的节点,只需要修改指针,而不需要移动指针,性能比vector大大提高。
总结一下,list本质就是双向环形链表,不支持随机访问,插入删除操作开销小。另外,由于每次加入新节点时都需要分配内存,然后拷贝元素,如果是在尾部操作元素,则性能反而比vector要低。
最后剩下deque了,deque可以大致认为是vector和list的混合体。如果学过操作系统内存管理,那么就很容易理解了。vector类似于分区存储管理方式,list类似链式存储方式,而deque则类似于分页式存储管理方式(当然由于deque是整用整取,因此不存在内部碎片问题)。deque就像是分页式存储,首先需要一个map表(相当于页表),存储各个node的指针,每个node都是一个连续的缓冲区,用于实际保存元素,并且还有一些预留空间。需要查找某一个元素时,首先需要计算这个元素处于哪个node缓冲区,然后计算offset从该缓冲区中定位。逻辑上看起来deque也是连续存储的,但事实是由一段段连续的空间组合起来的。deque上的迭代器主要依靠_M_cur,_M_first,_M_last,_M_node四个指针来操作,_M_cur指向当前元素,_M_first指向缓冲区头,_M_last指向缓冲区尾,_M_node指向map表,为了逻辑上看起来是连续的,deque迭代器实现了各种运算符重载, 比如++ / + / --/ -等,虽然++看起来简单,实际需要一些计算,如果_M_cur所在缓冲区还有元素,则直接移动_M_cur即可,若移到该缓冲区尾部,则需要找到下一个缓冲区,然后指向第一个元素。其他操作类似。而要访问任意一个位置的元素时,需要先计算该元素在哪个缓冲区,然后计算offset偏移量找到该元素。由于已经实现了+ -操作,则只需要简单调用*(begin() + n)即可。这就非常类似vector了。因此deque逻辑上实现了和vetor一样的随机访问,时间复杂度也是O(1),不过相对于vector,由于需要一些计算,效率上有点损失,不过这可以忽略不计。下面看看性能方面。
首先访问,上面已经说了,逻辑上支持随机访问。插入元素到指定位置,所在缓冲区还有预留空间,直接插入,并且该缓冲区需要移动元素,这比vector效率大大提高,因为不需要移动整个列表,而只是所在的缓冲区node。而如果所有缓冲区都满了,需要重新分配一个缓冲区,并把新元素放到新的缓冲区中,不需要额外的拷贝操作,只需修改map即可。如果只需要在头尾新增元素,尾部类似vector,看看还有没有预留空间,首部的话也类似,看看前面有没有预留空间,没有再分配一个缓冲区。因此deque在首部增删操作优于vector。
总结一下,deque可以看作是vector和list的折中方案,逻辑上实现了随机访问,在首尾操作开销很小。因此如果既要随机访问,又要各种插入删除操作,deque是一种不错的选择。
注意所有容器存储的都是原来元素的副本,而不是本身,因此不支持复制和赋值的元素类型不能放入容器中,比如IO类型对象就不能放入容器中,数组也不应该放入数组中,因此数组不支持直接赋值运算,即int a[5];int b[5] = a是非法的。如果容器放入的元素类型拷贝和析构开销都比较大,则应该考虑下放入元素的指针或许更好。
只有知道容器的内部实现原理,才能真正明智的选择容器的类型。
使用cpp STL都不会不知道迭代器,那迭代器到底是什么呢?
在http://www.cplusplus.com 中对迭代器的定义为:An iterator is any object that, pointing to some element in a range of elements (such as an array or a container), has the ability to iterate through the elements of that range using a set of operators (with at least the increment (++<) operators). 即迭代器就是指向一系列元素中的某一个元素,它能通过一系列操作迭代访问其中的元素。另外,需要注意的是cpp中的迭代器虽然绝大多数的本质就是指针,但并不是所有的迭代器都是指针,也有可能是其他形式,并且不保证迭代器具有指针的所有功能,比如input 和 output iterator就是一种单向受限的迭代器,执行--操作没有任何意义。cpp中,根据迭代器实现的不同功能,大体可以把迭代器分为5大类,具体各个类支持什么操作,查看迭代器。
现在重点看看cpp中vector中的迭代器,vector的迭代器是一个random access iterator,即随机访问迭代器,具有指针的所有操作。现在看看sgi源码中关于迭代器的实现(stl 3.3)。首先看看iterator的类型定义:
typedef _Tp value_type; typedef value_type* iterator; typedef const value_type* const_iterator;
其中_Tp是范型参数类型,比如声明vector<int>,则_Tp为int,根据类型定义,很显然vector中的iterator 本质就是一个指针,vector<int>::iterator其实就是int *类型。
看vector的方法begin() 和 end()
iterator begin() { return _M_start; } const_iterator begin() const { return _M_start; } iterator end() { return _M_finish; } const_iterator end() const { return _M_finish; }
可以看到,begin和end方法直接返回_M_start 和_M_finish,这两个私有属性就是iterator类型,即指针,前者指示。由于vector实现本质就是一个动态数组,_M_start则指示数组第一个元素,而_M_finish则指示数组最后一个元素的下一个单元,注意是下一个元素,因此_M_finish指向的单元不能访问,只是一个哨兵。vector更新时(比如初始化、push_back,pop_back等)都需要不断的维护这两个指针(实际上是3个,还有个_M_end_of_storage用于指示当前已分配内存的最后一个单元),而vector中的很多方法也是需要利用这两个指针的,比如以下方法:
size_type size() const { return size_type(end() - begin()); } size_type capacity() const { return size_type(_M_end_of_storage - begin()); } bool empty() const { return begin() == end(); }
我们也可以发现,既然是迭代器指针,那就存在不安全性,有可能越界访问,更有可能所引用的迭代器是失效的,即在返回迭代器后,又对容器进行了修改,而这些都需要用户自己注意,库并没有对这些进行检查。java在这方面会比较好点。
根据以上模拟一个简单的vector(只有push_back 和 pop_back方法),对vector原理会更清楚些:
template <class T> class vector { public: typedef T value_type; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef value_type* iterator; typedef value_type* pointer; typedef value_type& reference; iterator begin() { return M_start; } iterator end() { return M_end; } size_type size() { return size_type(end() - begin()); } size_type capacity() { return size_type(end_of_storage-begin()); } bool empty() { return begin() == end(); } reference operator[](size_type n) { return *(begin() + n); } vector() { M_start = M_end = end_of_storage = 0; } vector(size_type n) { M_start = new T[n](); M_end = M_start; end_of_storage = M_start + n; } ~vector() { destroy(begin(), end()); delete[] M_start; } void push_back(value_type i) { if (M_end < end_of_storage) { *M_end++ = i; return; } size_type old_capacity = capacity(); size_type new_capacity = old_capacity == 0 ? 16 : old_capacity << 1; iterator new_M_start = new T[new_capacity](); iterator new_M_end = new_M_start; iterator iter; for(iter = begin(); iter != end(); ++iter) { *new_M_end++ = *iter; } delete [] M_start; M_start = new_M_start; M_end = new_M_end; end_of_storage = M_start + new_capacity; *M_end++ = i; } void pop_back() { if (begin() == end()) return; --M_end; destroy(M_end); } private: iterator M_start; iterator M_end; iterator end_of_storage; void destroy(pointer p) { p->~T(); } void destroy(iterator start, iterator end) { iterator iter; for (iter = start; iter != end; iter++) { iter->~T(); } } };
分配内存时按照原有内存2倍关系分配,即如果当前分配了8B, 则下次分配16B,这样能够减少频繁内存分配,提高性能。可以不难看出,在访问元素时,无论是用迭代器还是[ ]运算符,都没有进行越界检测。
看看java的迭代器,主要是看看与cpp中vector迭代器的不同。
java支持迭代,必须实现Iterator接口,该接口定义为:
public interface Iterator<E> { boolean hasNext(); E next(); void remove(); }
该接口只有三个方法,实现该接口必须实现这三个方法。 下面看看ArrayList如何实现该接口,从而了解其原理。 ArrayList 的iterator方法:
public Iterator<E> iterator() { return Itr(); }
看来只好再看看ArrayList的基类 AbstractList,可以发现其实 Itr是AbstractList的私有内部类,其实现为:
private class Itr implements Iterator<E> { /** * Index of element to be returned by subsequent call to next. */ int cursor = 0; /** * Index of element returned by most recent call to next or * previous. Reset to -1 if this element is deleted by a call * to remove. */ int lastRet = -1; /** * The modCount value that the iterator believes that the backing * List should have. If this expectation is violated, the iterator * has detected concurrent modification. */ int expectedModCount = modCount; public boolean hasNext() { return cursor != size(); } public E next() { /* 检查列表是否被修改过 */ checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { /* 调用外部类的remove方法 */ AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
我们发现java中的迭代器是通过实现Iterator接口实现的,为了方便访问内部属性,一般声明为一个实现Iterator的内部类,因为内部类能够访问外部类的所有属性和方法。而实现方法也很简单,就是用一个cursor游标标识当前位置。我们也发现,java中迭代器比cpp中迭代器更安全,首先能够保证不会越界访问,另外还会检查是否原来的实例被修改过,即保证迭代器是有效的。
学过c语言的都知道,如果两个字符指针char *s1, char *s2(假设已初始化过), 运算符== 是比较两个字符的首地址是否相同(java语言字符串比较则是引用是否相同),而不是比较本身字符串是否相等。我们也知道在采用段式存储管理中,局部自动变量是存储在栈上的,动态分配则在堆上分配,而局部常量往往存储在代码段,java 中jvm为了优化存储,专门有一个string pool,用于存储字符串常量,并且保证相同的字符串只有一个拷贝。而全局已初始化变量或者静态变量往往存储在数据段,未初始则存在在BSS段中。这些都是学过语言的一些基本常识。
那么根据以上知识,看看下面的代码:
#include <stdio.h> char *s4 = "hello"; char *s5 = "hello"; const int g = 5; const int h = 5; const int aa[] = {1, 2, 3}; const int bb[] = {1, 2, 3}; int main(int argc, char **argv) { char *s1 = "hello"; char *s2 = "hello"; const char s3[] = "hello"; int a = 5; int b = 5; const int c = 5; const int d = 5; static const int e = 5; static const int f = 5; printf("%d\n", s1 == s2); /* 1 */ printf("%d\n", s2 == s3); /* 0 */ printf("%d\n", s4 == s5); /* 1 */ printf("%d\n", &a == &b); /* 0 */ printf("%d\n", &c == &d); /* 0 */ printf("%d\n", &e == &f); /* 0 */ printf("%d\n", &g == &h); /* 0 */ printf("%d\n", aa == bb); /* 0 */ return 0; }
首先s1和s2是相同的字符串常量,保存在代码段,并且是只可读的,不允许修改,由程序的输出(编译时用的gcc),可知类似java的string pool,为了优化存储,对于相同的字符串,只有一个副本。s3是一个字符数组,虽然被声明为const只读,但我们还是可以认为s3是一个变量,只是认为加上了const变量,致使s3指向的内存不可修改。注意s1和s2是字面字符串,固化在代码段中,无论如何都不能修改,它不是人为控制的,而是编译器决定的。s4和s5显然虽然本身是全局变量,应该存储在数据段,但指向的内存却是字面字符串常量,因此实际上指向的是和s1,s2一样的字符串,即s1, s2, s4,s5指向同一个字符串,他们的值(即指向的内存地址)是一样的。
讨论过了字符串常量,那其他常量是否也有像字符串一样的特性呢?c 和 d 显然是常量,但类似s3它也可以看成是受限的变量.因此他们的地址是不同的。aa和bb也类似。
现在看一段java代码:
class Main { public static void main(String[] args) { String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); /* true */ System.out.println(s1 + s2 == "hellohello");/* ? */ final String s3 = "hello"; final String s4 = "hello"; System.out.println(s3 == s4); /* true */ System.out.println(s3 + s4 == "hellohello"); /* ? */ System.out.println(s1 == s3); /* true */ final String s5 = new String("hello"); final String s6 = new String("hello"); System.out.println(s5 == s6); /* false */ } }
java中 == 运算符是比较引用对象是否相同。为了存储优化,在方法区的常量池中有一块用于存储字符串常量的区,称为String Pool,并且在编译时就已经确定,保存在.class文件中,若有多个相同的变量引用相同的对象,池中只会存在一份拷贝,即在池中创建对象时,先看看池中是否存在值相同的常量,存在直接返回引用,不存在时在真正创建。因此显然s1 和 s2 是相同的引用,返回true。我们也知道,java中String对象是不允许修改的,可以看作是final的,任何对String对象进行操作,均返回一个新的对象,其本身并没有改变。s1 + s2显然需要创建新的String 对象,为了效率,首先会利用s1 new 一个StringBuilder对象,然后执行append操作,最后再调用toString方法返回新的String对象,因此这个新的对象应该是在堆中或栈中,而不在池中,而"hellohello"是在池中的,它们显然不是同一个对象。注意s1和s2引用的对象是常量,不允许修改,但他们本身还是变量,能够引用其他对象。
然而s3和s4被声明为final变量,即s3 和 s4本身也是不可修改的,一旦初始化,不能再引用其他对象。关键在于此时s3 + s4 == "hellohello"返回true还是false呢?java中声明为final的变量会被初始化为编译时常量(compile-time constant expression),当使用这些变量时其实只是内联过去,相当于替换,或者类似c中的宏,因此声明为final的s3和s3, s3 + s4 实际上运行时只能看到"hello" + "hello",依然是字符串常量,注意s1和s2编译时是无法确定它的值的,因此编译完后我们仍然看到的是s1 + s2。因此s1 + s2 == "hellohello"返回false,而s3 + s4返回true。
而s5和s6实际先在池中创建了一个字符串常量(若池中存在,则直接引用),然后再调用String的构造方法又创建了一个新的对象,这个对象是在堆上分配的,因此实际new String("string")创建了两个String对象。当然有一个没有用了,会由垃圾回收清理掉。
Fabrice Bellard仅仅只用447B代码, 计算出已知最大的素数, 大约有1700万位。
int m=1711276033,N=1,t[1<<25]={2},a,*p,i,e=39717691,s,c,U=1;g(d,h){for(i=s;i<1<< 24;i*=2)d=d*1LL*d%m;for(p=t;p<t+N;p+=s)for(i=s,c=1;i;i--)a=p[s]*(h?c:1LL)%m,p[s] =(m*1U+*p-a)*(h?1LL:c)%m,*p=(a*1U+*p)%m,p++,c=c*1LL*d%m;}main(){while(e/=2){N*=2 ;U=U*1LL*(m+1)/2%m;for(s=N;s/=2;)g(40,0);for(p=t;p<t+N;p++)*p=*p*1LL**p%m*U%m; for(s=1;s<N;s*=2)g(983983719,1);for(a=0,p=t;p<t+N;)a+=*p<<(e&1),*p++=a%10,a/=10; }while(!*--p);for(t[0]--;p>=t;)putchar(48+*p--);}
编译器要求支持64B的long long 类型,Linux 下gcc编译成功,2.3Ghz 双核CPU大约需要2分钟的运行时间。
double d = 1.2 - (long)1.2; d *= 10; printf("%d\n", (int)d);
面试时遇到这个问题,问输出是多少? 大多数人会立马回答输出是2,但实际上输出并不是凭空臆想的结果。 其实我们都知道,浮点数在计算机中只能近似表示,存在误差,计算机中根本不存在确切的0.0,即说明我们不能直接比较浮点数是否等于0.0,即
#include <stdio.h> int main(int argc, char **argv) { double d = (1.1 - 1.0) * 10 - 1; printf("%lf\n", d); /* d = 0.000000 */ if (d == 0) printf("true\n"); else printf("false\n"); return 0; }
会返回false! 而应该使用fabs(f1, f2) <= precision 进行比较,注意这个precision是绝对误差,当f1和f2在这个绝对误差附近时可能出现问题!所以开头那段程序的输出可能是2,也可能是1!
今天整nodejs实在不行,npm和nodejs在源上版本不兼容,install一直出问题。于是不想整了。 写了下很久没有写的c代码,好吧。。无聊!在标准库stdlib中以下函数均已实现
int atoi(const char *nptr); long atol(const char *nptr); long long atoll(const char *nptr); long long atoq(const char *nptr); double atof(const char *nptr); double strtod(const char *nptr, char **endptr); float strtof(const char *nptr, char **endptr); long double strtold(const char *nptr, char **endptr);
今天无聊,自己写了几个实现下:
#include <stdio.h> #include <ctype.h> #include <string.h> #include <stdlib.h> #define MAX_LENGTH 15 void swap(char *a, char *b) { char t = *a; *a = *b; *b = t; return; } int atoi(const char *s) { int len = strlen(s); int res = 0; int i = 0; int sign = 0; if(s[0] == '-') sign = i = 1; for (; i < len; ++i) { if (!isdigit(s[i])) { fprintf(stderr, "%s is not a Integer!\n", s); exit(1); } res *= 10; res += (s[i] - '0'); } return sign == 1 ? -res : res; } char *itoa(const int n, char *dest) { int m = n; int i, j; int cur = 0; int sign = 0; memset(dest, 0, sizeof(char) * MAX_LENGTH); if (m < 0) { dest[cur++] = '-'; m *= -1; sign = 1; } while (m) { i = m % 10; dest[cur++] = '0' + i; m /= 10; } dest[cur] = 0; j = strlen(dest) - 1; i = sign ? 1 : 0; for(; i != j; i++, j--) swap(&dest[i], &dest[j]); return dest; } double atof(const char *s) { int len = strlen(s); int i = 0; int sign = 0; /* 符号位, 负数1,正数0 */ int int_part = 0; /* 整数部分 */ double point_part = 0; /* 小数部分 */ double res; /* 最后结果数值部分,不包括符号位 */ if (s[0] == '-') sign = i = 1; for (; i < len && s[i] != '.'; ++i) { if (!isdigit(s[i])) { fprintf(stderr, "%s is not a number!\n", s); exit(1); } int_part *= 10; int_part += (s[i] - '0'); } double base = 0.1; /* 当前小数位 */ for (++i; i < len; i++) { if (!isdigit(s[i])) { fprintf(stderr, "%s is not a number!\n", s); exit(1); } point_part += ((s[i] - '0') * base); base *= 0.1; } res = int_part + point_part; return sign ? -res : res; }