C++ Programming Mastery Guide
Complete guide from OOP basics to modern C++ features
The Complete C++ Programming Mastery Guide
Master object-oriented programming, templates, STL, and modern C++ features for high-performance applications
Understanding C++: Power and Performance
C++ is a powerful, high-performance programming language created by Bjarne Stroustrup as an extension of C. It combines low-level memory manipulation with high-level object-oriented features, making it ideal for system software, game development, high-frequency trading, and performance-critical applications.
What makes C++ unique is its "zero-cost abstractions" philosophy - you don't pay for features you don't use. Modern C++ (C++11 and beyond) introduces powerful features like smart pointers, lambda expressions, and move semantics while maintaining backward compatibility.
Industry Powerhouse: C++ powers major software like Google Chrome, Microsoft Windows, Adobe Creative Suite, Unreal Engine, and MySQL. Its performance makes it the language of choice for applications where speed and efficiency are critical.
1. C++ Basics & OOP Fundamentals
Hello World: Your First C++ Program
// hello.cpp #include <iostream> // Input/output stream #include <string> // String class using namespace std; // Use standard namespace int main() { cout << "Hello, World!" << endl; cout << "Welcome to C++ Programming!" << endl; // String manipulation string name = "Alice"; cout << "Hello, " << name << "!" << endl; // Input from user int age; cout << "Enter your age: "; cin >> age; cout << "You are " << age << " years old." << endl; return 0; } // Compile and run: // g++ hello.cpp -o hello // ./hello
C++ Enhancements Over C
#include <iostream> using namespace std; // Function overloading - same name, different parameters void print(int value) { cout << "Integer: " << value << endl; } void print(double value) { cout << "Double: " << value << endl; } void print(const string& value) { cout << "String: " << value << endl; } // Default parameters void greet(string name, string greeting = "Hello") { cout << greeting << ", " << name << "!" << endl; } // References (safer alternative to pointers) void swap(int& a, int& b) { int temp = a; a = b; b = temp; } int main() { // C++ specific types bool isCppFun = true; cout << "Is C++ fun? " << boolalpha << isCppFun << endl; // Function overloading demonstration print(42); print(3.14159); print("Hello C++"); // Default parameters greet("Alice"); greet("Bob", "Good morning"); // References int x = 10, y = 20; cout << "Before swap: x=" << x << ", y=" << y << endl; swap(x, y); cout << "After swap: x=" << x << ", y=" << y << endl; // Reference variables int original = 100; int& ref = original; // ref is an alias for original ref = 200; cout << "original: " << original << ", ref: " << ref << endl; return 0; }
Namespaces and Scope Resolution
#include <iostream> #include <string> // Custom namespace namespace MathOperations { const double PI = 3.14159; double add(double a, double b) { return a + b; } double multiply(double a, double b) { return a * b; } // Nested namespace namespace Geometry { double circleArea(double radius) { return PI * radius * radius; } } } // Global variable int globalVar = 100; // Another namespace namespace StringUtils { void printLength(const std::string& str) { std::cout << "Length: " << str.length() << std::endl; } } int main() { // Using namespace members std::cout << "PI: " << MathOperations::PI << std::endl; std::cout << "5 + 3 = " << MathOperations::add(5, 3) << std::endl; std::cout << "Circle area: " << MathOperations::Geometry::circleArea(5.0) << std::endl; // Using directive using StringUtils::printLength; printLength("Hello C++"); // Scope resolution for global variables int globalVar = 50; // Local variable shadows global std::cout << "Local globalVar: " << globalVar << std::endl; std::cout << "Global globalVar: " << ::globalVar << std::endl; // Creating alias for long namespace namespace MO = MathOperations; std::cout << "Using alias: " << MO::multiply(4, 5) << std::endl; return 0; }
Memory Management: new and delete
#include <iostream> using namespace std; class Point { public: int x, y; Point(int x = 0, int y = 0) : x(x), y(y) { cout << "Point constructed: (" << x << ", " << y << ")" << endl; } ~Point() { cout << "Point destroyed: (" << x << ", " << y << ")" << endl; } }; int main() { // Single object allocation Point* p1 = new Point(10, 20); cout << "p1: (" << p1->x << ", " << p1->y << ")" << endl; // Array allocation int size = 5; int* arr = new int[size]; for (int i = 0; i < size; i++) { arr[i] = (i + 1) * 10; } cout << "Array elements: "; for (int i = 0; i < size; i++) { cout << arr[i] << " "; } cout << endl; // Array of objects Point* points = new Point[3] { Point(1, 1), Point(2, 2), Point(3, 3) }; // Memory deallocation delete p1; // Single object delete[] arr; // Array of primitives delete[] points; // Array of objects // Memory allocation with initialization int* value = new int(42); // Initialize with 42 cout << "Initialized value: " << *value << endl; delete value; // Checking for allocation failure (modern approach uses exceptions) try { int* hugeArray = new int[1000000000000LL]; // Very large allocation delete[] hugeArray; } catch (const bad_alloc& e) { cout << "Memory allocation failed: " << e.what() << endl; } return 0; }
2. Classes & Object-Oriented Programming
Class Basics: Encapsulation and Abstraction
#include <iostream> #include <string> using namespace std; class BankAccount { private: string accountNumber; string accountHolder; double balance; // Private helper method bool isValidAmount(double amount) const { return amount > 0; } public: // Constructor BankAccount(const string& number, const string& holder, double initialBalance = 0.0) : accountNumber(number), accountHolder(holder), balance(initialBalance) { cout << "Account created for " << holder << endl; } // Destructor ~BankAccount() { cout << "Account " << accountNumber << " is being closed" << endl; } // Public interface void deposit(double amount) { if (isValidAmount(amount)) { balance += amount; cout << "Deposited: $" << amount << endl; } else { cout << "Invalid deposit amount!" << endl; } } bool withdraw(double amount) { if (isValidAmount(amount) && amount <= balance) { balance -= amount; cout << "Withdrawn: $" << amount << endl; return true; } cout << "Withdrawal failed!" << endl; return false; } // Const member function - doesn't modify object state double getBalance() const { return balance; } void displayInfo() const { cout << "Account: " << accountNumber << endl; cout << "Holder: " << accountHolder << endl; cout << "Balance: $" << balance << endl; } // Static member static string getBankName() { return "C++ Bank"; } }; int main() { // Creating objects BankAccount account1("123456", "Alice", 1000.0); BankAccount account2("789012", "Bob"); // Using object methods account1.displayInfo(); account1.deposit(500.0); account1.withdraw(200.0); account1.withdraw(2000.0); // Should fail cout << "Current balance: $" << account1.getBalance() << endl; // Static member access cout << "Bank name: " << BankAccount::getBankName() << endl; return 0; // Destructors called automatically when objects go out of scope }
Inheritance and Polymorphism
#include <iostream> #include <string> #include <vector> using namespace std; // Base class class Shape { protected: string name; string color; public: Shape(const string& n, const string& c) : name(n), color(c) {} // Virtual function for polymorphism virtual double area() const = 0; // Pure virtual function virtual void display() const { cout << "Shape: " << name << ", Color: " << color; } // Virtual destructor for proper cleanup virtual ~Shape() { cout << "Shape " << name << " destroyed" << endl; } }; // Derived class class Circle : public Shape { private: double radius; public: Circle(const string& n, const string& c, double r) : Shape(n, c), radius(r) {} // Override base class function double area() const override { return 3.14159 * radius * radius; } void display() const override { Shape::display(); cout << ", Radius: " << radius << ", Area: " << area() << endl; } }; class Rectangle : public Shape { private: double width, height; public: Rectangle(const string& n, const string& c, double w, double h) : Shape(n, c), width(w), height(h) {} double area() const override { return width * height; } void display() const override { Shape::display(); cout << ", Width: " << width << ", Height: " << height << ", Area: " << area() << endl; } }; int main() { // Polymorphism in action vector<Shape*> shapes; shapes.push_back(new Circle("Circle1", "Red", 5.0)); shapes.push_back(new Rectangle("Rect1", "Blue", 4.0, 6.0)); shapes.push_back(new Circle("Circle2", "Green", 3.0)); cout << "Polymorphic behavior:" << endl; for (Shape* shape : shapes) { shape->display(); // Calls appropriate derived class function } // Cleanup for (Shape* shape : shapes) { delete shape; } // Object slicing demonstration Circle circle("MyCircle", "Yellow", 10.0); Shape shape = circle; // Object slicing - Circle part is lost shape.display(); cout << endl; return 0; }
Operator Overloading
#include <iostream> #include <cmath> using namespace std; class Vector3D { private: double x, y, z; public: // Constructors Vector3D(double x = 0, double y = 0, double z = 0) : x(x), y(y), z(z) {} // Operator overloading Vector3D operator+(const Vector3D& other) const { return Vector3D(x + other.x, y + other.y, z + other.z); } Vector3D operator-(const Vector3D& other) const { return Vector3D(x - other.x, y - other.y, z - other.z); } // Dot product double operator*(const Vector3D& other) const { return x * other.x + y * other.y + z * other.z; } // Scalar multiplication Vector3D operator*(double scalar) const { return Vector3D(x * scalar, y * scalar, z * scalar); } // Friend function for commutative scalar multiplication friend Vector3D operator*(double scalar, const Vector3D& vec); // Compound assignment Vector3D& operator+=(const Vector3D& other) { x += other.x; y += other.y; z += other.z; return *this; } // Comparison operators bool operator==(const Vector3D& other) const { return x == other.x && y == other.y && z == other.z; } bool operator!=(const Vector3D& other) const { return !(*this == other); } // Subscript operator double operator[](int index) const { switch(index) { case 0: return x; case 1: return y; case 2: return z; default: throw out_of_range("Index out of range"); } } // Output stream operator friend ostream& operator<<(ostream& os, const Vector3D& vec); // Magnitude double magnitude() const { return sqrt(x*x + y*y + z*z); } // Normalize Vector3D normalize() const { double mag = magnitude(); if (mag == 0) return *this; return *this * (1.0 / mag); } }; // Friend function definitions Vector3D operator*(double scalar, const Vector3D& vec) { return vec * scalar; // Reuse member function } ostream& operator<<(ostream& os, const Vector3D& vec) { os << "(" << vec.x << ", " << vec.y << ", " << vec.z << ")"; return os; } int main() { Vector3D v1(1, 2, 3); Vector3D v2(4, 5, 6); cout << "v1 = " << v1 << endl; cout << "v2 = " << v2 << endl; Vector3D v3 = v1 + v2; cout << "v1 + v2 = " << v3 << endl; Vector3D v4 = v1 - v2; cout << "v1 - v2 = " << v4 << endl; double dot = v1 * v2; cout << "v1 · v2 = " << dot << endl; Vector3D v5 = v1 * 2.5; cout << "v1 * 2.5 = " << v5 << endl; Vector3D v6 = 3.0 * v1; cout << "3.0 * v1 = " << v6 << endl; v1 += v2; cout << "v1 after += v2: " << v1 << endl; cout << "v1 magnitude: " << v1.magnitude() << endl; cout << "v1 normalized: " << v1.normalize() << endl; cout << "v1[0] = " << v1[0] << ", v1[1] = " << v1[1] << ", v1[2] = " << v1[2] << endl; return 0; }
Friend Functions and Static Members
#include <iostream> #include <vector> using namespace std; class Student { private: string name; int id; double gpa; // Static member - shared by all Student objects static int totalStudents; static int nextId; public: Student(const string& n, double g) : name(n), gpa(g), id(nextId++) { totalStudents++; } ~Student() { totalStudents--; } // Friend function declaration friend void displayStudentInfo(const Student& s); // Friend class friend class StudentManager; // Static member functions static int getTotalStudents() { return totalStudents; } static void resetIds() { nextId = 1000; // Start IDs from 1000 } // Regular member functions string getName() const { return name; } double getGpa() const { return gpa; } int getId() const { return id; } }; // Static member definitions int Student::totalStudents = 0; int Student::nextId = 1000; // Friend function definition void displayStudentInfo(const Student& s) { cout << "Student ID: " << s.id << endl; // Can access private members cout << "Name: " << s.name << endl; cout << "GPA: " << s.gpa << endl; } // Friend class class StudentManager { private: vector<Student*> students; public: void addStudent(Student* student) { students.push_back(student); } void displayAllStudents() { cout << "=== All Students ===" << endl; for (Student* s : students) { // Can access private members because friend cout << s->id << ": " << s->name << " (GPA: " << s->gpa << ")" << endl; } } double getAverageGPA() const { if (students.empty()) return 0.0; double total = 0.0; for (Student* s : students) { total += s->gpa; // Access private member } return total / students.size(); } }; int main() { // Demonstrate static members cout << "Initial total students: " << Student::getTotalStudents() << endl; Student s1("Alice", 3.8); Student s2("Bob", 3.2); Student s3("Charlie", 3.9); cout << "After creating 3 students: " << Student::getTotalStudents() << endl; // Using friend function cout << "\nUsing friend function:" << endl; displayStudentInfo(s1); // Using friend class StudentManager manager; manager.addStudent(&s1); manager.addStudent(&s2); manager.addStudent(&s3); manager.displayAllStudents(); cout << "Average GPA: " << manager.getAverageGPA() << endl; // Demonstrate object destruction { Student temp("Temporary", 2.5); cout << "Total students in block: " << Student::getTotalStudents() << endl; } // temp destroyed here cout << "After block: " << Student::getTotalStudents() << endl; return 0; }
3. Templates & Generic Programming
Function Templates
#include <iostream> #include <string> #include <vector> using namespace std; // Basic function template template<typename T> T max(const T& a, const T& b) { return (a > b) ? a : b; } // Template with multiple types template<typename T1, typename T2> void printPair(const T1& first, const T2& second) { cout << "(" << first << ", " << second << ")" << endl; } // Template specialization template<> const char* max(const char* const & a, const char* const & b) { return (strcmp(a, b) > 0) ? a : b; } // Template with non-type parameters template<typename T, int size> class FixedArray { private: T data[size]; public: T& operator[](int index) { if (index < 0 || index >= size) { throw out_of_range("Index out of bounds"); } return data[index]; } const T& operator[](int index) const { if (index < 0 || index >= size) { throw out_of_range("Index out of bounds"); } return data[index]; } int getSize() const { return size; } }; // Variadic templates (C++11) template<typename T> void print(const T& value) { cout << value << endl; } template<typename T, typename... Args> void print(const T& first, const Args&... rest) { cout << first << " "; print(rest...); } int main() { // Using function templates cout << "max(5, 10) = " << max(5, 10) << endl; cout << "max(3.14, 2.71) = " << max(3.14, 2.71) << endl; cout << "max('a', 'z') = " << max('a', 'z') << endl; string s1 = "hello", s2 = "world"; cout << "max(strings) = " << max(s1, s2) << endl; // Using specialized version for C-strings cout << "max(C-strings) = " << max("apple", "banana") << endl; // Multiple type parameters printPair(42, "answer"); printPair(3.14, "pi"); // Non-type template parameters FixedArray<int, 5> intArray; for (int i = 0; i < intArray.getSize(); i++) { intArray[i] = (i + 1) * 10; } cout << "Fixed array: "; for (int i = 0; i < intArray.getSize(); i++) { cout << intArray[i] << " "; } cout << endl; // Variadic templates print(1, 2, 3, "hello", 4.5, 'X'); return 0; }
Class Templates
#include <iostream> #include <vector> #include <stdexcept> using namespace std; // Basic class template template<typename T> class Stack { private: vector<T> elements; public: // Push element onto stack void push(const T& value) { elements.push_back(value); } // Pop element from stack void pop() { if (elements.empty()) { throw out_of_range("Stack<>::pop(): empty stack"); } elements.pop_back(); } // Get top element T top() const { if (elements.empty()) { throw out_of_range("Stack<>::top(): empty stack"); } return elements.back(); } // Check if stack is empty bool empty() const { return elements.empty(); } // Get size size_t size() const { return elements.size(); } }; // Template with default parameters template<typename T = int, int initialCapacity = 10> class DynamicArray { private: T* data; int capacity; int count; public: DynamicArray() : capacity(initialCapacity), count(0) { data = new T[capacity]; } ~DynamicArray() { delete[] data; } void add(const T& value) { if (count >= capacity) { // Resize capacity *= 2; T* newData = new T[capacity]; for (int i = 0; i < count; i++) { newData[i] = data[i]; } delete[] data; data = newData; } data[count++] = value; } T& operator[](int index) { if (index < 0 || index >= count) { throw out_of_range("Index out of bounds"); } return data[index]; } int size() const { return count; } int getCapacity() const { return capacity; } }; // Template inheritance template<typename T> class LoggingStack : public Stack<T> { public: void push(const T& value) { cout << "Pushing: " << value << endl; Stack<T>::push(value); } void pop() { if (!this->empty()) { cout << "Popping: " << this->top() << endl; Stack<T>::pop(); } } }; int main() { // Using class template Stack<int> intStack; intStack.push(10); intStack.push(20); intStack.push(30); cout << "Stack size: " << intStack.size() << endl; cout << "Top element: " << intStack.top() << endl; intStack.pop(); cout << "After pop, top: " << intStack.top() << endl; // Stack with strings Stack<string> stringStack; stringStack.push("hello"); stringStack.push("world"); cout << "String stack top: " << stringStack.top() << endl; // Using template with default parameters DynamicArray<> defaultArray; // Uses default types (int, capacity 10) DynamicArray<double, 5> doubleArray; for (int i = 0; i < 15; i++) { defaultArray.add(i * 10); } cout << "Default array size: " << defaultArray.size() << ", capacity: " << defaultArray.getCapacity() << endl; // Using derived template class LoggingStack<int> loggingStack; loggingStack.push(100); loggingStack.push(200); loggingStack.pop(); return 0; }
4. STL Containers & Algorithms
STL Containers: Vector, List, Map
#include <iostream> #include <vector> #include <list> #include <map> #include <unordered_map> #include <set> #include <algorithm> #include <string> using namespace std; void demonstrateVector() { cout << "=== VECTOR ===" << endl; vector<int> vec = {1, 2, 3, 4, 5}; // Adding elements vec.push_back(6); vec.insert(vec.begin() + 2, 99); // Accessing elements cout << "Element at index 2: " << vec[2] << endl; cout << "Front: " << vec.front() << ", Back: " << vec.back() << endl; // Iterating cout << "Elements: "; for (auto it = vec.begin(); it != vec.end(); ++it) { cout << *it << " "; } cout << endl; // Range-based for loop (C++11) cout << "Range-based: "; for (int value : vec) { cout << value << " "; } cout << endl; // Capacity cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << endl; } void demonstrateList() { cout << "\n=== LIST ===" << endl; list<string> names = {"Alice", "Bob", "Charlie"}; names.push_front("First"); names.push_back("Last"); // List-specific operations names.sort(); names.unique(); cout << "Names: "; for (const auto& name : names) { cout << name << " "; } cout << endl; } void demonstrateMap() { cout << "\n=== MAP ===" << endl; map<string, int> ageMap = { {"Alice", 25}, {"Bob", 30}, {"Charlie", 35} }; // Inserting ageMap["Diana"] = 28; ageMap.insert({"Eve", 32}); // Accessing cout << "Alice's age: " << ageMap["Alice"] << endl; // Iterating through map cout << "All ages:" << endl; for (const auto& pair : ageMap) { cout << pair.first << ": " << pair.second << endl; } // Finding elements auto it = ageMap.find("Bob"); if (it != ageMap.end()) { cout << "Found Bob: " << it->second << endl; } } void demonstrateAlgorithms() { cout << "\n=== ALGORITHMS ===" << endl; vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6}; // Sorting sort(numbers.begin(), numbers.end()); cout << "Sorted: "; for (int n : numbers) cout << n << " "; cout << endl; // Finding auto found = find(numbers.begin(), numbers.end(), 7); if (found != numbers.end()) { cout << "Found 7 at position: " << distance(numbers.begin(), found) << endl; } // Counting int countFives = count(numbers.begin(), numbers.end(), 5); cout << "Number of 5s: " << countFives << endl; // Lambda expressions with algorithms (C++11) cout << "Even numbers: "; for_each(numbers.begin(), numbers.end(), [](int n) { if (n % 2 == 0) cout << n << " "; }); cout << endl; // Transform vector<int> squared; transform(numbers.begin(), numbers.end(), back_inserter(squared), [](int n) { return n * n; }); cout << "Squared: "; for (int n : squared) cout << n << " "; cout << endl; } int main() { demonstrateVector(); demonstrateList(); demonstrateMap(); demonstrateAlgorithms(); return 0; }
Smart Pointers (C++11 and beyond)
#include <iostream> #include <memory> #include <vector> using namespace std; class Resource { private: string name; public: Resource(const string& n) : name(n) { cout << "Resource " << name << " created" << endl; } ~Resource() { cout << "Resource " << name << " destroyed" << endl; } void use() { cout << "Using resource " << name << endl; } string getName() const { return name; } }; void demonstrateUniquePtr() { cout << "=== UNIQUE_PTR ===" << endl; // Creating unique_ptr unique_ptr<Resource> res1 = make_unique<Resource>("Unique1"); res1->use(); // Transfer ownership (move semantics) unique_ptr<Resource> res2 = move(res1); if (!res1) { cout << "res1 is now empty" << endl; } res2->use(); // unique_ptr in containers vector<unique_ptr<Resource>> resources; resources.push_back(make_unique<Resource>("Vector1")); resources.push_back(make_unique<Resource>("Vector2")); for (const auto& res : resources) { res->use(); } } // Resources automatically destroyed here void demonstrateSharedPtr() { cout << "\n=== SHARED_PTR ===" << endl; // Creating shared_ptr shared_ptr<Resource> res1 = make_shared<Resource>("Shared1"); cout << "Use count: " << res1.use_count() << endl; { shared_ptr<Resource> res2 = res1; // Share ownership cout << "Use count after sharing: " << res1.use_count() << endl; res2->use(); } // res2 destroyed, but resource remains cout << "Use count after res2 destruction: " << res1.use_count() << endl; res1->use(); } void demonstrateWeakPtr() { cout << "\n=== WEAK_PTR ===" << endl; shared_ptr<Resource> shared = make_shared<Resource>("WeakDemo"); weak_ptr<Resource> weak = shared; cout << "Shared use count: " << shared.use_count() << endl; // Using weak_ptr if (auto temp = weak.lock()) { cout << "Resource is alive: " << temp->getName() << endl; } else { cout << "Resource has been destroyed" << endl; } // Reset shared pointer shared.reset(); cout << "After reset, shared use count: " << shared.use_count() << endl; if (auto temp = weak.lock()) { cout << "Resource is still alive" << endl; } else { cout << "Resource has been destroyed" << endl; } } // Custom deleter for unique_ptr void customDeleter(Resource* res) { cout << "Custom deleter called for " << res->getName() << endl; delete res; } int main() { demonstrateUniquePtr(); demonstrateSharedPtr(); demonstrateWeakPtr(); // Using custom deleter cout << "\n=== CUSTOM DELETER ===" << endl; unique_ptr<Resource, decltype(&customDeleter)> customRes( new Resource("Custom"), customDeleter); customRes->use(); return 0; }
💻 C++ Practice Projects
Beginner Level
- 1Create a Bank Account Management System with classes
- 2Build a Simple Calculator with operator overloading
- 3Implement a Student Grade Management System
- 4Create a Vector Mathematics Library with operator overloading
- 5Build a Contact Management System with file I/O
Intermediate Level
- 1Develop a Template-based Stack and Queue library
- 2Create a Smart Pointer implementation from scratch
- 3Build a Matrix Class with arithmetic operations
- 4Implement a Custom String Class with dynamic memory
- 5Create a Generic Sorting Algorithm library
Advanced Level
- 1Build a Multithreaded Task Scheduler
- 2Create a Memory Pool Allocator with custom new/delete
- 3Implement a Simple Game Engine with polymorphism
- 4Develop a Template Metaprogramming library
- 5Build a Network Server with RAII and smart pointers
📋 C++ Quick Reference
Modern C++ Features
- •auto - Type deduction
- •lambda - Anonymous functions
- •range-based for - Simplified loops
- •smart pointers - Automatic memory management
- •move semantics - Efficient resource transfer
- •constexpr - Compile-time evaluation
- •nullptr - Type-safe null pointer
STL Containers
- •vector - Dynamic array
- •list - Doubly-linked list
- •map - Sorted key-value pairs
- •unordered_map - Hash table
- •set - Unique sorted elements
- •queue - FIFO container
- •stack - LIFO container
Master High-Performance Programming!
C++ remains the language of choice for performance-critical applications, game development, system software, and high-frequency trading. Its unique combination of high-level abstractions and low-level control provides unparalleled power and efficiency.
Modern C++ features make the language safer and more expressive while maintaining the performance that made C++ famous. Mastering C++ opens doors to cutting-edge software development careers.