Software Engineering Documentation

Welcome to Software Engineering Documentation! This book is a comprehensive collection of notes, tutorials, and reference materials covering a wide range of topics in software engineering. Whether you're diving into Rust, C++, C#, or exploring the expansive realms of Azure and AWS cloud services, this documentation is designed to guide you through the intricacies of modern software development.

Purpose

This book serves two main purposes:

  • Personal Reference: A curated set of notes and examples to help review and reinforce key concepts in software engineering.
  • Open Source Resource: An accessible, community-driven guide for anyone interested in learning about or contributing to the world of software engineering.

What You'll Find Inside

  • In-depth Explanations: Detailed insights into various programming languages and cloud platforms.
  • Practical Code Examples: Real-world code snippets to illustrate key concepts and best practices.
  • Best Practices: Industry standards and tips gathered from personal experiences and ongoing research.
  • Ongoing Updates: Continuous improvements and additions based on new discoveries and community feedback.

Whether you're a student, a professional developer, or simply curious about the world of software engineering, this documentation is here to support your journey. Let's explore, learn, and build together!

Happy coding!

AI + Machine Learning


Table of Contents


Azure Machine Learning

Azure Machine Learning
  • Purpose: Provides an end-to-end machine learning platform for data scientists and developers.
  • Key Capabilities:
    • Model training, deployment, and management.
    • Automated Machine Learning (AutoML) for no-code/low-code solutions.
    • Integration with popular frameworks such as TensorFlow, PyTorch, and Scikit-learn.
  • Typical Use Cases:
    • Predictive analytics
    • Fraud detection
    • Recommendation systems

Azure AI Services

Azure AI Services
  • Purpose: Collection of AI-based services for vision, speech, language understanding, and more.
  • Key Capabilities:
    • Cognitive Services for text analytics, speech-to-text, image recognition, etc.
    • Prebuilt AI models for language translation, anomaly detection, sentiment analysis.
  • Typical Use Cases:
    • Chatbots and virtual assistants
    • Document processing and OCR
    • Audio/video transcription

Azure AI Foundry

Azure AI Foundry
  • Purpose: Helps organizations build, deploy, and operationalize AI solutions rapidly.
  • Key Capabilities:
    • Collaborative environment for data science teams.
    • Accelerators and industry-specific solution templates.
    • End-to-end MLOps to streamline AI solution lifecycle.
  • Typical Use Cases:
    • Data science collaboration
    • Rapid AI prototyping
    • Automated CI/CD for AI projects

Azure OpenAI Service

Azure OpenAI
  • Purpose: Provides access to advanced language models (e.g., GPT series) for building AI solutions.
  • Key Capabilities:
    • Natural language processing (NLP) and generation.
    • Code generation, text summarization, translation, etc.
    • Easily integrates with other Azure services for end-to-end solutions.
  • Typical Use Cases:
    • Chatbots with contextual awareness
    • Intelligent document analysis
    • Code completion or generation

Azure AI Search
  • Purpose: Intelligent search and indexing service powered by AI.
  • Key Capabilities:
    • Natural language processing search queries.
    • Cognitive skills for image, OCR, and text analytics.
    • Synonym search and ranking capabilities.
  • Typical Use Cases:
    • Website and application content search
    • Enterprise data search with AI-driven insights
    • E-commerce product search

Compute


Table of Contents


Azure Virtual Machines

Azure Virtual Machines
  • Purpose: On-demand, scalable computing resources in the cloud.
  • Key Capabilities:
    • Wide range of VM sizes, regions, and OS options (Windows, Linux).
    • Support for custom images and templates.
    • Integration with Virtual Networks, storage, and other Azure services.
  • Typical Use Cases:
    • Lift-and-shift of existing applications to the cloud.
    • Development and testing environments.
    • Host custom software and workloads.

Azure Virtual Machine Scale Sets

Azure Virtual Machine Scale Sets
  • Purpose: Automatically scale virtual machines to handle demand changes.
  • Key Capabilities:
    • Automatic VM provisioning and de-provisioning.
    • Load balancing integrations.
    • Uniform or flexible orchestration modes.
  • Typical Use Cases:
    • Large-scale compute clusters.
    • Stateless web tiers with variable traffic.
    • Big data and containerized workloads.

Azure App Service

Azure App Service
  • Purpose: Fully managed platform for building, deploying, and scaling web apps and APIs.
  • Key Capabilities:
    • Supports multiple languages and frameworks (e.g., .NET, Node.js, Python, Java).
    • Continuous integration and deployment (CI/CD).
    • Built-in autoscaling, monitoring, and security features.
  • Typical Use Cases:
    • Enterprise web applications
    • RESTful API hosting
    • Minimal management overhead for web deployments

Azure Kubernetes Service (AKS)

Azure Kubernetes Service
  • Purpose: Managed Kubernetes service to run containerized applications at scale.
  • Key Capabilities:
    • Automated provisioning and cluster management.
    • Integration with Azure Container Registry (ACR) for container images.
    • Autoscaling, rolling updates, and built-in monitoring.
  • Typical Use Cases:
    • Microservices-based architectures.
    • Container orchestration for development and production.
    • Hybrid and multi-cloud strategies (with Azure Arc).

Azure Virtual Desktop

Azure Virtual Desktop
  • Purpose: Provide Windows 10/11 desktops and apps in the cloud.
  • Key Capabilities:
    • Multi-session Windows 10/11 environment.
    • Optimizations for Microsoft 365 Apps.
    • Integration with Azure Active Directory (Microsoft Entra ID).
  • Typical Use Cases:
    • Remote work scenarios.
    • Secure access to corporate desktops and applications.
    • Disaster recovery for on-premises desktop environments.

Azure Functions

Azure Functions
  • Purpose: Serverless compute platform for event-driven, on-demand code execution.
  • Key Capabilities:
    • Pay-per-execution billing model (consumption plan).
    • Triggers and bindings to integrate with other Azure services.
    • Built-in scaling and managed infrastructure.
  • Typical Use Cases:
    • Real-time file processing
    • IoT data processing
    • Webhooks and microservices

Azure Container Instances (ACI)

Azure Container Instances
  • Purpose: Run containers without managing servers or clusters.
  • Key Capabilities:
    • Fast, isolated compute for containerized workloads.
    • Per-second billing.
    • Integrates with Virtual Networks for secure deployments.
  • Typical Use Cases:
    • Batch processing
    • Test and development containers
    • Event-driven container workloads

Azure Batch

Azure Batch
  • Purpose: Large-scale parallel and high-performance computing (HPC) in the cloud.
  • Key Capabilities:
    • Automatically manage and scale compute nodes.
    • Integration with Azure Storage for input/output data.
    • Supports Docker containers and common HPC frameworks.
  • Typical Use Cases:
    • Rendering 3D images or simulations
    • Financial risk modeling
    • Genomic and scientific research

Management and Governance


Table of Contents


Azure Subscriptions

Azure Subscription
  • Purpose: Logical container for Azure resources, billing, and usage.
  • Key Capabilities:
    • Payment and billing boundary.
    • Resource access and usage quotas.
    • Linked to Azure Active Directory tenant.
  • Typical Use Cases:
    • Organizing workloads by department or project.
    • Enforcing budgetary constraints and spending thresholds.

Azure Management Groups

Azure Management Groups
  • Purpose: Hierarchical grouping of subscriptions for organizational governance.
  • Key Capabilities:
    • Apply policies and access controls across multiple subscriptions.
    • Organize subscriptions by departments, regions, or functions.
  • Typical Use Cases:
    • Enterprise-wide compliance policies
    • Consistent resource governance

Azure Policy

Azure Policy
  • Purpose: Enforce compliance and governance across resources.
  • Key Capabilities:
    • Policy assignments for resource configurations (e.g., allowed regions, tag requirements).
    • Monitoring and remediation of non-compliant resources.
  • Typical Use Cases:
    • Regulatory compliance (e.g., ISO, PCI).
    • Resource standardization and cost control.

Azure Blueprints

Azure Blueprints
  • Purpose: Package and deploy sets of resource templates and policies at scale.
  • Key Capabilities:
    • Create repeatable environments with ARM templates, policies, and RBAC.
    • Versioning for consistent environment deployments.
  • Typical Use Cases:
    • Enterprise environment setup
    • Standardized development/test/production deployments

Azure Resource Groups

Azure Resource Groups
  • Purpose: Logical grouping of Azure resources for management, deployment, and monitoring.
  • Key Capabilities:
    • All resources in a group share a common lifecycle.
    • Role-based access control at the resource group level.
  • Typical Use Cases:
    • Grouping related resources for an application.
    • Easier deployment and deletion of entire stacks.

Azure Tags

Azure Tags
  • Purpose: Metadata labels to categorize resources (e.g., cost center, environment).
  • Key Capabilities:
    • Use tags for resource organization and cost allocation.
    • Query resources by tags in Azure Portal and CLI.
  • Typical Use Cases:
    • Cost management and chargeback.
    • Resource discovery and governance.

Azure Arc

Azure Arc
  • Purpose: Extend Azure management and services to on-premises, multi-cloud, and edge environments.
  • Key Capabilities:
    • Manage servers, Kubernetes clusters, and data services anywhere.
    • Consistent policy enforcement and governance outside Azure.
  • Typical Use Cases:
    • Hybrid cloud strategies
    • Central management across diverse environments

Azure Resource Manager (ARM) Templates

Azure Templates
  • Purpose: Infrastructure-as-Code for deploying and managing Azure resources declaratively.
  • Key Capabilities:
    • JSON-based templates to define infrastructure configurations.
    • Parameterization for dynamic deployments.
    • Consistent, repeatable environment provisioning.
  • Typical Use Cases:
    • Automated deployments with CI/CD.
    • Version-controlled infrastructure configurations.

Azure Purview

Azure Purview
  • Purpose: Unified data governance service to manage and control data across on-premises, multi-cloud, and SaaS sources.
  • Key Capabilities:
    • Automated data discovery and classification.
    • Data lineage for end-to-end tracking.
    • Built-in data catalog for enterprise-wide data visibility.
  • Typical Use Cases:
    • Regulatory compliance (e.g., GDPR).
    • Centralized data governance and discovery.
    • Enterprise-wide data cataloging.

Azure Advisor

Azure Advisor
  • Purpose: Personalized recommendations for best practices, cost optimization, performance, and security.
  • Key Capabilities:
    • Identifies idle resources, performance bottlenecks, and potential security risks.
    • Recommends corrective actions.
  • Typical Use Cases:
    • Ongoing cost optimization.
    • Performance tuning and reliability improvements.
    • Security posture assessments.

Monitoring


Table of Contents


Azure Monitor

Azure Monitor
  • Purpose: Centralized monitoring service for collecting, analyzing, and acting on telemetry from cloud and on-premises environments.
  • Key Capabilities:
    • Metrics, logs, and application insights.
    • Alerts and automated remediation.
    • Visualization with dashboards and workbooks.
  • Typical Use Cases:
    • Infrastructure and application performance monitoring.
    • Log analytics for troubleshooting.
    • Proactive alerting and incident management.

Azure Service Health

Azure Service Health
  • Purpose: Personalized dashboard of Azure service issues and maintenance events.
  • Key Capabilities:
    • Real-time updates on service outages, planned maintenance.
    • Alerts and notifications via various channels.
  • Typical Use Cases:
    • Tracking regional availability issues.
    • Planning around maintenance windows.

Network


Table of Contents


Azure VNet

Azure Virtual Network
  • Purpose: Fundamental building block for private networking in Azure.
  • Key Capabilities:
    • Isolated network segments within Azure.
    • Subnets for segmented network design.
    • Network security boundaries and controls.
  • Typical Use Cases:
    • Host Azure resources securely.
    • Extend on-premises networks to the cloud.
    • Control inbound and outbound traffic routing.

Azure Subnets

Azure Subnet
  • Purpose: Logical subdivisions of an Azure VNet for resource segmentation.
  • Key Capabilities:
    • Control network traffic routes and isolation.
    • Enforce security boundaries with Network Security Groups (NSGs).
  • Typical Use Cases:
    • Micro-segmentation of workloads.
    • Separation of front-end, application, and database tiers.

Azure Network Interface (NIC)

Azure Network Interfaces
  • Purpose: Connects a VM to a VNet, assigning private and/or public IP addresses.
  • Key Capabilities:
    • One or multiple NICs per VM (dependent on VM size).
    • IP configurations for inbound/outbound traffic.
  • Typical Use Cases:
    • Assigning multiple IP addresses to different subnets.
    • Multi-homing scenarios.

Azure Network Security Groups (NSG)

Azure Network Security Groups
  • Purpose: Control inbound and outbound traffic at the subnet or NIC level.
  • Key Capabilities:
    • Rule-based filtering with priority.
    • State-aware inspection of TCP traffic.
  • Typical Use Cases:
    • Restricting access to specific ports or protocols.
    • Filtering traffic at scale for multi-tier applications.

Azure Virtual Network Peering

Azure Virtual Network Peering
  • Purpose: Connect two Azure VNets for direct network traffic flow.
  • Key Capabilities:
    • Low latency, high-bandwidth interconnection.
    • No gateway or public internet traversal.
  • Typical Use Cases:
    • Merging networks across different regions or subscriptions.
    • Multi-team or multi-environment connectivity.

Azure DNS

Azure DNS
  • Purpose: Host DNS domains and manage DNS records in Azure.
  • Key Capabilities:
    • Internal and external DNS zone hosting.
    • Integration with Azure-based services and resources.
  • Typical Use Cases:
    • Custom domain mapping for public-facing apps.
    • Private DNS zones for internal name resolution.

Azure ExpressRoute

Azure ExpressRoute
  • Purpose: Dedicated private connection from on-premises networks to Azure.
  • Key Capabilities:
    • Higher security, reliability, and speed than typical VPN.
    • Bandwidth options up to 100 Gbps.
  • Typical Use Cases:
    • High-throughput data replication.
    • Enterprise-scale hybrid connections.

Azure Virtual Network Gateways

Azure Virtual Network Gateways
  • Purpose: Provide gateways for VPN or ExpressRoute connections.
  • Key Capabilities:
    • Configurable gateway SKUs for different throughput levels.
    • Site-to-Site, Point-to-Site, and ExpressRoute gateway options.
  • Typical Use Cases:
    • Secure VPN tunnels to on-premises resources.
    • Hybrid cloud scenarios.

Azure Load Balancer

Azure Load Balancer
  • Purpose: Distribute network traffic across multiple VMs.
  • Key Capabilities:
    • Layer 4 (TCP/UDP) load balancing.
    • High availability and network-level redundancy.
  • Typical Use Cases:
    • Balancing web server workloads.
    • Ensuring high availability for backend services.

Azure Application Gateway

Azure Application Gateway
  • Purpose: Application-level (Layer 7) load balancing and web application firewall (WAF).
  • Key Capabilities:
    • SSL offloading, session affinity, path-based routing.
    • Integrated WAF for security.
  • Typical Use Cases:
    • Secure, scalable web application hosting.
    • Advanced traffic routing rules for microservices.

Azure Front Door

Azure Front Door
  • Purpose: Global, scalable entry point for web applications with intelligent routing.
  • Key Capabilities:
    • Layer 7 reverse proxy with edge computing capabilities.
    • Global load balancing, caching, SSL offloading.
  • Typical Use Cases:
    • Distributing traffic across multiple regions.
    • Accelerated content delivery with edge POPs.

Azure Private Link
  • Purpose: Securely connect services within Azure over a private endpoint.
  • Key Capabilities:
    • Private connectivity to Azure PaaS services (e.g., Storage, SQL).
    • Eliminates exposure to public internet.
  • Typical Use Cases:
    • High-security networks with restricted internet access.
    • Compliance with strict data governance requirements.

Azure Bastion

Azure Bastion
  • Purpose: Securely connect to Azure VMs without public IP addresses.
  • Key Capabilities:
    • Browser-based RDP/SSH over SSL.
    • Fully managed PaaS service in the Azure portal.
  • Typical Use Cases:
    • Remote administration of VMs in a locked-down network.
    • Enforcing just-in-time access without exposing RDP/SSH ports publicly.

Security


Table of Contents


Microsoft Entra ID

Microsoft Defender for Cloud
  • Purpose: Identity and access management service (formerly Azure Active Directory).
  • Key Capabilities:
    • User and group management, SSO, conditional access.
    • Integration with on-premises Active Directory for hybrid identity.
  • Typical Use Cases:
    • Centralized identity for cloud apps.
    • Enterprise security with conditional access policies.
    • User provisioning and lifecycle management.

Multi-Factor Authentication (MFA)

Multi-Factor Authentication
  • Purpose: Adds a second layer of security to user sign-ins and transactions.
  • Key Capabilities:
    • Verifications via phone call, SMS, mobile app notifications.
    • Conditional access integration (IP restrictions, device compliance).
  • Typical Use Cases:
    • Securing remote workforce.
    • Protecting privileged accounts.

Microsoft Defender for Cloud

Microsoft Defender for Cloud
  • Purpose: Unified security management and threat protection across hybrid environments.
  • Key Capabilities:
    • Security posture assessment (Secure Score).
    • Threat detection and response (powered by Azure Security Center).
    • Integration with Azure Sentinel (SIEM) for advanced threat analytics.
  • Typical Use Cases:
    • Detecting and mitigating security threats in Azure and on-premises.
    • Compliance checks for PCI, ISO, HIPAA, etc.
    • Centralized security policy management.

Storage


Table of Contents


Azure Disks

Azure Disks
  • Purpose: Persistent storage for Azure VMs.
  • Key Capabilities:
    • Managed or unmanaged disk options.
    • Different performance tiers (Standard HDD, Standard SSD, Premium SSD, Ultra Disk).
  • Typical Use Cases:
    • VM operating system and data disks.
    • High-performance storage for I/O-intensive workloads.

Azure Blob Containers

Azure Blob Containers
  • Purpose: Object storage solution for unstructured data.
  • Key Capabilities:
    • Hot, Cool, and Archive tiers for cost optimization.
    • Scalable, cost-effective data storage for images, videos, backups.
  • Typical Use Cases:
    • Data lake storage for analytics.
    • Media content delivery.
    • Backup and disaster recovery.

Azure File Shares

Azure File Shares
  • Purpose: Fully managed file share service using the SMB or NFS protocol.
  • Key Capabilities:
    • Shared file access across multiple VMs or on-premises.
    • Integration with Active Directory for access control.
  • Typical Use Cases:
    • Lift-and-shift legacy applications that use file shares.
    • Centralized file storage and collaboration.

Azure Queues

Azure Queues
  • Purpose: Messaging service for decoupling and scaling application components.
  • Key Capabilities:
    • Simple, asynchronous message queue.
    • Durable storage of messages.
  • Typical Use Cases:
    • Microservices communication.
    • Asynchronous task processing.

Azure Tables

Azure Tables
  • Purpose: NoSQL key-value store for high-volume data.
  • Key Capabilities:
    • Schemaless design.
    • Scalable and cost-effective for large datasets.
  • Typical Use Cases:
    • IoT data ingestion.
    • Storing user profiles or session data.

Azure Data Box

Azure Data Box
  • Purpose: Physical devices to securely transfer large amounts of data to Azure.
  • Key Capabilities:
    • Various device sizes and types.
    • Encryption in transit and at rest.
  • Typical Use Cases:
    • One-time data migration or bulk data import.
    • Offline data transfer when network bandwidth is limited.

Azure Data Lake Storage

Azure Data Lake Storage
  • Purpose: Hyperscale repository for big data analytics workloads.
  • Key Capabilities:
    • High-performance, hierarchical file system for analytics.
    • Integration with Azure analytics services (Databricks, Synapse).
  • Typical Use Cases:
    • Data lake for enterprise analytics pipelines.
    • Big data processing and machine learning.

Array / String


Table of Contents


13. Roman to Integer

  • LeetCode Link: Roman to Integer
  • Difficulty: Easy
  • Topic(s): Hash Table, String, Math

🧠 Problem Statement

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

For example, 2 is written as II in Roman numeral, just two ones added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

  • I can be placed before V (5) and X (10) to make 4 and 9.
  • X can be placed before L (50) and C (100) to make 40 and 90.
  • C can be placed before D (500) and M (1000) to make 400 and 900.

Given a roman numeral, convert it to an integer.

Example 1:

Input: s = "III"
Output: 3
Explanation: III = 3.

Example 2:

Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 3.

Example 3:

Input: s = "MCMXCIV"
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

🧩 Approach

  1. Create a mapping of Roman numeral symbols to their integer values.
  2. Initialize a result variable to 0.
  3. Iterate through the string:
    • If the current symbol is less than the next symbol, subtract its value from the result.
    • Otherwise, add its value to the result.
  4. Return the result.

💡 Solution

def romanToInt(self, s: str) -> int:
    """
    Convert a Roman numeral to an integer.

    Args:
        s (str): The Roman numeral string.

    Returns:
        int: The integer representation of the Roman numeral.
    """
    romans: Dict[str, int] = {
            "I": 1,
            "V": 5,
            "X": 10,
            "L": 50,
            "C": 100,
            "D": 500,
            "M": 1000
    }
    res: int = 0
    for i in range(len(s)):
        if i + 1 < len(s) and romans[s[i]] < romans[s[i + 1]]:
            res -= romans[s[i]]
        else:
            res += romans[s[i]]
    return res

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(1)

14. Longest Common Prefix

🧠 Problem Statement

Write a function to find the longest common prefix string amongst an array of strings.

If there is no common prefix, return an empty string "".

Example 1:

Input: strs = ["flower","flow","flight"]
Output: "fl"

Example 2:

Input: strs = ["dog","racecar","car"]
Output: ""
Explanation: There is no common prefix among the input strings.

🧩 Approach

  1. Initialize the result as an empty string.
  2. Iterate through the characters of the first string.
  3. For each character, check if it matches the corresponding character in all other strings.
  4. If a mismatch is found or if the end of any string is reached, return the result.
  5. If all characters match, append the character to the result.
  6. Return the result after checking all characters of the first string.

💡 Solution

def longestCommonPrefix(self, strs: List[str]) -> str:
    """
    Find the longest common prefix among an array of strings.

    Args:
        strs (List[str]): The list of strings.

    Returns:
        str: The longest common prefix.
    """
    res: str = ""
    for i in range(len(strs[0])):
        for s in strs:
            if i == len(s) or s[i] != strs[0][i]:
                return res
        res += strs[0][i]
    return res

🧮 Complexity Analysis

  • Time Complexity: O(n * m)
  • Space Complexity: O(1)

26. Remove Duplicates from Sorted Array

🧠 Problem Statement

Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place such that each unique element appears only once. The relative order of the elements should be kept the same. Then return the number of unique elements in nums.

Consider the number of unique elements of nums to be k, to get accepted, you need to do the following things:

  • Change the array nums such that the first k elements of nums contain the unique elements in the order they were present in nums initially. The remaining elements of nums are not important as well as the size of nums.
  • Return k.

Example 1:

Input: nums = [1,1,2]
Output: 2, nums = [1,2,_]
Explanation: Your function should return k = 2, with the first two elements of nums being 1 and 2 respectively.
It does not matter what you leave beyond the returned k (hence they are underscores).

Example 2:

Input: nums = [0,0,1,1,1,2,2,3,3,4]
Output: 5, nums = [0,1,2,3,4,_,_,_,_,_]
Explanation: Your function should return k = 5, with the first five elements of nums being 0, 1, 2, 3, and 4 respectively.
It does not matter what you leave beyond the returned k (hence they are underscores).

🧩 Approach

Use the two-pointer technique:

  • i: slow pointer, tracks the position of the last unique element.
  • j: fast pointer, scans the array.

Steps:

  1. Initialize i = 0.
  2. Iterate j from 1 to end of array.
  3. If nums[j] != nums[i], it’s a new unique element:
    • Increment i
    • Copy nums[j] to nums[i]
  4. After the loop, the first i + 1 elements are the unique values.

💡 Solution

def removeDuplicates(self, nums: List[int]) -> int:
    """
    Remove duplicates from a sorted array in-place and return the new length.

    Args:
        nums (List[int]): The input sorted array.

    Returns:
        int: The new length of the array after removing duplicates.
    """
    i: int = 0

    for j in range(1, len(nums)):
        if nums[j] != nums[i]:
            i += 1
            nums[i] = nums[j]

    return i + 1

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(1)

27. Remove Element

  • LeetCode Link: Remove Element
  • Difficulty: Easy
  • Topic(s): Array, Two Pointers

🧠 Problem Statement

Given an integer array nums and an integer val, remove all occurrences of val in nums in-place. The order of the elements may be changed. Then return the number of elements in nums that are not equal to val.

Consider the number of elements in nums which are not equal to val be k, to get accepted, you need to do the following things:

  • Change the array nums such that the first k elements of nums contain the elements which are not equal to val. The remaining elements of nums are not important as well as the size of nums.
  • Return k.

Example 1:

Input: nums = [3,2,2,3], val = 3
Output: 2, nums = [2,2,_,_]
Explanation: Your function should return k = 2, with the first two elements of nums being 2.
It does not matter what you leave beyond the returned k (hence they are underscores).

Example 2:

Input: nums = [0,1,2,2,3,0,4,2], val = 2
Output: 5, nums = [0,1,4,0,3,_,_,_]
Explanation: Your function should return k = 5, with the first five elements of nums containing 0, 0, 1, 3, and 4.
Note that the five elements can be returned in any order.
It does not matter what you leave beyond the returned k (hence they are underscores).

🧩 Approach

Use the two-pointer technique:

  • One pointer (i) scans every element.
  • Another pointer (k) keeps track of the position where the next valid (non-val) element should be placed.

Steps:

  1. Iterate through the array.
  2. If the current element is not equal to val, place it at index k and increment k.
  3. After the loop, k will represent the new length of the array with val removed.

💡 Solution

def removeElement(self, nums: List[int], val: int) -> int:
    """
    Remove all occurrences of `val` in `nums` in-place and return the new length.

    Args:
        nums (List[int]): The input array.
        val (int): The value to be removed.

    Returns:
        int: The new length of the array after removal.
    """
    k: int = 0
    for i in range(len(nums)):
        if nums[i] != val:
            nums[k] = nums[i]
            k += 1
    return k

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(1)

58. Length of Last Word

🧠 Problem Statement

Given a string s consisting of words and spaces, return the length of the last word in the string.

A word is a maximal substring consisting of non-space characters only.

Example 1:

Input: s = "Hello World"
Output: 5
Explanation: The last word is "World" with length 5.

Example 2:

Input: s = "   fly me   to   the moon  "
Output: 4
Explanation: The last word is "moon" with length 4.

Example 3:

Input: s = "luffy is still joyboy"
Output: 6
Explanation: The last word is "joyboy" with length 6.

🧩 Approach

  1. Split the string s into words using the split() method, which automatically handles multiple spaces.
  2. Return the length of the last word in the list of words.

💡 Solution

def lengthOfLastWord(self, s: str) -> int:
    """
    Calculate the length of the last word in a string.

    Args:
        s (str): The input string.

    Returns:
        int: The length of the last word.
    """
    words: List[str] = s.split()
    return len(words[-1])

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(n)

88. Merge Sorted Array

  • LeetCode Link: Merge Sorted Array
  • Difficulty: Easy
  • Topic(s): Array, Two Pointers, Sorting

🧠 Problem Statement

You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.

Merge nums1 and nums2 into a single array sorted in non-decreasing order.

The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.

Example 1:

Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
Output: [1,2,2,3,5,6]
Explanation: The arrays we are merging are [1,2,3] and [2,5,6].
The result of the merge is [1,2,2,3,5,6] with the underlined elements coming from nums1.

Example 2:

Input: nums1 = [1], m = 1, nums2 = [], n = 0
Output: [1]
Explanation: The arrays we are merging are [1] and [].
The result of the merge is [1].

Example 3:

Input: nums1 = [0], m = 0, nums2 = [1], n = 1
Output: [1]
Explanation: The arrays we are merging are [] and [1].
The result of the merge is [1].
Note that because m = 0, there are no elements in nums1. The 0 is only there to ensure the merge result can fit in nums1.

🧩 Approach

Use three pointers:

  • midx points to the last valid element in nums1 (m - 1)
  • nidx points to the last element in nums2 (n - 1)
  • right points to the last index in nums1 (m + n - 1)

Compare elements from the back and place the larger one at index right. Decrement pointers accordingly.

Repeat until nidx reaches 0 (no need to worry about midx, since the rest are already in place).

💡 Solution

def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
    """
    Merge two sorted arrays `nums1` and `nums2` into `nums1` in-place.

    Args:
        nums1 (List[int]): The first sorted array with enough space to hold the elements of `nums2`.
        m (int): The number of elements in `nums1`.
        nums2 (List[int]): The second sorted array.
        n (int): The number of elements in `nums2`.

    Returns:
        None: The result is stored in `nums1`.
    """
    midx: int = m - 1
    nidx: int = n - 1
    right: int = m + n - 1

    while nidx >= 0:
        if midx >= 0 and nums1[midx] > nums2[nidx]:
            nums1[right] = nums1[midx]
            midx -= 1
        else:
            nums1[right] = nums2[nidx]
            nidx -= 1

        right -= 1

🧮 Complexity Analysis

  • Time Complexity: O(m + n)
  • Space Complexity: O(1)

121. Best Time to Buy and Sell Stock

🧠 Problem Statement

You are given an array prices where prices[i] is the price of a given stock on the ith day.

You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example 1:

Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.

Example 2:

Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transactions are done and the max profit = 0.

🧩 Approach

Dynamic programming approach:

  1. Initialize profit to 0 and lowest to the first price.

  2. Iterate through the prices:

    • For each price, calculate the potential profit by subtracting lowest from the current price.
    • Update profit if the calculated profit is greater than the current profit.
    • Update lowest to be the minimum of the current price and lowest.
  3. Return the profit.

This approach ensures that we always consider the lowest price seen so far, allowing us to calculate the maximum profit efficiently.

💡 Solution

def maxProfit(self, prices: List[int]) -> int:
    """
    Calculate the maximum profit from a single buy and sell transaction.

    Args:
        prices (List[int]): The list of stock prices.

    Returns:
        int: The maximum profit achievable, or 0 if no profit can be made.
    """
    profit: int = 0
    lowest: int = prices[0]

    for price in prices:
        profit = max(profit, price - lowest)
        lowest = min(lowest, price)
    return profit

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(1)

169. Majority Element

  • LeetCode Link: Majority Element
  • Difficulty: Easy
  • Topic(s): Array, Hash Table, Divide and Conquer, Counting

🧠 Problem Statement

Given an array nums of size n, return the majority element.

The majority element is the element that appears more than ⌊n / 2⌋ times. You may assume that the majority element always exists in the array.

Example 1:

Input: nums = [3,2,3]
Output: 3

Example 2:

Input: nums = [2,2,1,1,1,2,2]
Output: 2

🧩 Approach

Boyer-Moore Voting Algorithm:

  1. Initialize a count = 0 and candidate = None.
  2. Iterate through the array:
    • If count == 0, set candidate = current element
    • If current element == candidate, increment count
    • Else, decrement count
  3. At the end, candidate is the majority element.

This works because the majority element appears more than all others combined.

💡 Solution

def majorityElement(self, nums: List[int]) -> int:
    """
    Find the majority element in an array using Boyer-Moore Voting Algorithm.

    Args:
        nums (List[int]): The input array.

    Returns:
        int: The majority element.
    """
    count: int = 0
    candidate: int = 0

    for num in nums:
        if count == 0:
            candidate = num

        if num == candidate:
            count += 1
        else:
            count -= 1

    return candidate

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(1)

Binary Tree


Table of Contents


104. Maximum Depth of Binary Tree

🧠 Problem Statement

Given the root of a binary tree, return its maximum depth.

A binary tree's maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

Example 1:

Input: root = [3,9,20,null,null,15,7]
Output: 3

Example 2:

Input: root = [1,null,2]
Output: 2

🧩 Approach

  • DFS (Depth-First Search):
    • Recursively calculate the depth of the left and right subtrees.
    • The maximum depth is 1 + max(depth of left subtree, depth of right subtree).

💡 Solution

def maxDepth(self, root: Optional[TreeNode]) -> int:
    """
    Calculate the maximum depth of a binary tree.

    Args:
        root (Optional[TreeNode]): The root node of the binary tree.

    Returns:
        int: The maximum depth of the binary tree.
    """
    if not root:
        return 0

    return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

🧮 Complexity Analysis

  • Time Complexity: O(n)
  • Space Complexity: O(h)

Standard I/O


Table of Contents


I/O Stream

  • Header <iostream>: This header provides basic I/O functionality using streams.
  • Standard Streams:
    • std::cin for standard input
    • std::cout for standard output
    • std::cerr for standard error
  • Operators:
    • Extraction (>>): Reads data from an input stream.
    • Insertion (<<): Writes data to an output stream.

Code Example:

#include <iostream>

int main() {
  std::cout << "Enter a number: ";
  int x;
  std::cin >> x;
  std::cout << "You entered: " << x << std::endl;
  return 0;
}

Namespace

  • namespace: Used to group related classes, functions, variables.
  • std namespace: Contains the standard library (e.g., std::cout, std::cin, std::string).
  • Usage:
    • using namespace std; brings all symbols in the std namespace into the current scope (not always recommended for large projects due to naming conflicts).
    • std:: prefix to explicitly qualify names.

Code Example:

#include <iostream>

namespace myNamespace {
  void printMessage() {
    std::cout << "Hello from myNamespace!" << std::endl;
  }
}

int main() {
  myNamespace::printMessage();
  return 0;
}

String

  • Header <string>: Provides the std::string class.
  • Basic Operations:
    • Construction, concatenation, length check, indexing.
    • Member functions like size(), length(), substr(), find(), etc.

Code Example:

#include <iostream>
#include <string>

int main() {
  std::string greeting = "Hello";
  std::string name;

  std::cout << "Enter your name: ";
  std::cin >> name;

  std::string message = greeting + ", " + name + "!";
  std::cout << message << std::endl;

  std::cout << "Message length: " << message.length() << std::endl;
  return 0;
}

Buffer

  • Buffer: A temporary storage area for data transfers.
  • I/O Streams are typically buffered to optimize reading/writing.
  • Flushing:
    • std::endl flushes the buffer after printing a newline.
    • std::flush can be used explicitly to flush the output buffer without a newline.

Code Example:

#include <iostream>

int main() {
  std::cout << "This will be printed immediately" << std::flush;
  // Some computation...
  std::cout << "\nNow we printed a newline and flushed the stream." << std::endl;
  return 0;
}

Object-Oriented Programming


Table of Contents


Principles of OOP

  1. Encapsulation

    • Wrapping data and methods into a single unit (class).
    • Implementation details hidden from the outside world.
  2. Inheritance

    • One class (derived class) acquires the properties and behaviors of another class (base class).
    • Promotes code reuse and hierarchical relationships.
  3. Polymorphism

    • Ability to take many forms.
    • Typically achieved via function overloading, operator overloading, and virtual functions.
  4. Abstraction

    • Exposing only essential features and hiding internal details.
    • Simplifies complexity by providing high-level interfaces.

Class Implementation

  • Class Declaration: Defined using class keyword.
  • Access Specifiers:
    • public: Accessible from anywhere.
    • private: Accessible only within the class.
    • protected: Accessible within the class and derived classes.

Code Example:

File: point.h

#ifndef POINT
#define POINT
#define SIZE 3

class Point {
  private:
    double x;
    double y;
    char label[SIZE];

  public:
    Point();
    Point(double a, double b);
    void setX(double value);
    void setY(double value);
    void setLabel(const char* s);
    double getX() const;
    double getY() const;
    void display();
};
#endif

File: point.cpp

#include <iostream>
#include <cstring>

#include "point.h"

Point::Point() : x(0), y(0) {}

Point::Point(double a, double b) : x(a), y(b) {}

void Point::setX(double value) {
  x = value;
}

void Point::setY(double value) {
  y = value;
}

void Point::setLabel(const char* s) {
  std::strcpy(label, s);
}

double Point::getX() const {
  return x;
}

double Point::getY() const {
  return y;
}

void Point::display() {
  std::cout << "Point label is: " << label;
  std::cout << "x coordinate is: " << x;
  std::cout << "y coordinate is: " << y << std::endl;
}

Pointer to Objects

  • Syntax: ClassName *ptr = &object;
  • Usage:
    • Access members using the arrow operator (->).
    • Dynamic allocation with new.

Code Example:

File: point.h

#ifndef POINT
#define POINT
#define SIZE 3

class Point {
  private:
    double x;
    double y;
    char label[SIZE];

  public:
    Point();
    Point(double a, double b);
    void setX(double value);
    void setY(double value);
    void set_label(const char* s);
    double getX() const;
    double getY() const;
    void display();
};
#endif

File: point.cpp

#include <iostream>

#include "point.h"

Point::Point() : x(0), y(0) {}

Point::Point(double a, double b) : x(a), y(b) {}

void Point::setX(double value) {
  x = value;
}

void Point::setY(double value) {
  y = value;
}

void Point::setLabel(const char* s) {
  std::strcpy(label, s);
}

double Point::getX() const {
  return x;
}

double Point::getY() const {
  return y;
}

void Point::display() {
  std::cout << "Point label is: " << label;
  std::cout << "x coordinate is: " << x;
  std::cout << "y coordinate is: " << y << std::endl;
}

void print(const Point* p, const Point& r) {
  std::cout << p->getX();
  std::cout << p->getY();


  std::cout << r.getX();
  std::cout << r.getY();
}

int main() {
    Point a;
    a.setX(120);
    a.setY(200);
    print(&a, a);
}

Constructor

  • Purpose: Initialize objects upon creation.
  • Types of Constructors:
    • Default Constructor: No parameters.
    • Parameterized Constructor: One or more parameters.
    • Copy Constructor: Initializes an object using another object of the same class.

Code Example:

File: point.h

#ifndef POINT
#define POINT
class Point {
  private:
    double x;
    double y;

  public:
    Point();
    Point(double a, double b);

};
#endif

File: point.cpp

#include "point.h"

Point::Point() : x(0), y(0) {}

// This is also possible. However the other method is preferred.
// Point::Point() {
//   x = 0;
//   y = 0;
// }

Point::Point(double a, double b) : x(a), y(b) {}

// This is also possible. However the other method is preferred.
// Point::Point(double a, double b) {
//   x = a;
//   y = b;
// }

int main() {
    Point a;
    Point b(6, 7);
}

this Pointer

  • Definition: An implicit pointer to the current object.
  • Usage: Commonly used when member variable names are shadowed by parameters, or to return the current object.

Code Example:

File: counter.h

#ifndef COUNTER
#define COUNTER
class Counter {
  private:
    int value;

  public:
    Counter();
    void increment(int n);
};
#endif

File: counter.cpp

#include "counter.h"

Counter::Counter() : value(0) {}

void Counter::increment(int n) {
  this->value += n;
}

int main() {
  Counter x;
  Counter y;
  x.increment(5);
  y.increment(6);
}

Array of Objects

  • You can create arrays of class objects on the stack or heap.
  • Access each element like a normal array element.

Code Example:

File: car.h

#ifndef CAR
#define CAR
#define SIZE 20

class Car {
  private:
    char make[SIZE];
    int year;
    double price;

  public:
    Car();
    Car(const char* m, int y, double p);
    const char* getMake() const;
    void setMake(const char* m);
    int getYear() const;
    void setYear(int y);
    double getPrice() const;
    void setPrice(double p);
};
#endif

File: car.cpp

#include <cstring>
#include <iostream>

#include "car.h"

Car::Car() : year(0), price(0) {
  for (int j = 0; j < SIZE; j++) {
    this->make[j] = "\0";
  }
}

Car::Car(const char* m, int y, double p) : year(y), price(p) {
  assert(strlen(m) < SIZE);
  strcpy(this->make, m);
}

const char* Car::getMake() const {
  return this->make;
}

void Car::setMake(const char* m) {
  assert(strlen(m) < SIZE);
  strcpy(this->make, m);
}

int Car::getYear() const {
  return this->year;
}

void Car::setYear(int y) {
  this->year = y;
}

double Car::getPrice() const {
  return this->price;
}

void Car::setPrice(double p) {
  this->price = p;
}

void displayAll(Car x[], int n) {
  for (int j = 0; j < n; j++) {
    std::cout << x[j].getMake();
  }
}

void swap(Car *x, Car *y) {
  Car temp;
  temp = *x;
  *x = *y;
  *y = temp;
}

int main() {
  Car x[3];
  x[0].setMake("Honda");
  x[1].setMake("Ford");
  displayAll(x, 2);
  swap(&x[0], &x[1])
}

Dynamic Allocation and De-allocation of Memory

  • new: Allocates memory on the heap.
  • delete: Frees memory previously allocated with new.
  • new[] and delete[] for arrays.

Code Example:

File: car.h

#ifndef CAR
#define CAR
#define SIZE 20

class Car {
  private:
    char make[SIZE];
    int year;
    double price;

  public:
    Car();
    Car(const char* m, int y, double p);
    const char* getMake() const;
    void setMake(const char* m);
    int getYear() const;
    void setYear(int y);
    double getPrice() const;
    void setPrice(double p);
};
#endif

File: car.cpp

#include <cstring>

#include "car.h"

Car::Car() : year(0), price(0) {
  for (int j = 0; j < SIZE; j++) {
    this->make[j] = "\0";
  }
}

Car::Car(const char* m, int y, double p) : year(y), price(p) {
  assert(strlen(m) < SIZE);
  strcpy(this->make, m);
}

const char* Car::getMake() const {
  return this->make;
}

void Car::setMake(const char* m) {
  assert(strlen(m) < SIZE);
  strcpy(this->make, m);
}

int Car::getYear() const {
  return this->year;
}

void Car::setYear(int y) {
  this->year = y;
}

double Car::getPrice() const {
  return this->price;
}

void Car::setPrice(double p) {
  this->price = p;
}

int main() {
  int *array;
  array = new int[2];
  array[0] = 79;
  array[1] = 99;
  delete[] array;

  Car *x;
  Car *y;
  x = new Car;
  y = new Car[3];
  delete x;
  delete[] y;
}

Destructor

  • Definition: A special member function called when an object goes out of scope or is deleted.
  • Syntax: ~ClassName()
  • Purpose: Clean up resources, close files, release memory.

Code Example:

File: person.h

#ifndef PERON
#define PERSON
class Person {
  private:
    int age;
    char* name;

  public:
    Person(const char* n, int a);
    ~Person();
    const char* getName() const;
    void setName(const char* n);
    int getAge() const;
    void setAge(int a);
};
#endif

File: person.cpp

#include <string.h>
#include <iostream>

#include "person.h"

Person::Person(const char* n, int a) : age(a) {
  this->name = new char[strlen(n) + 1];
  assert(this->name != 0);
  strcpy(this->name, m);
}

Person::~Person() {
  delete[] this->name;
  this->name = NULL;
}

const char* Person::getName() const {
  return this->name;
}

void Person::setName(const char* n) {
  assert(strlen(n) <= strlen(this->name));
  strcpy(this->name, n);
}

int Person::getAge() const {
  return this->age;
}

void Person::setAge(int a) {
  this->age = y;
}

int main() {
  Person x("Alice", 14);
  std::cout << x.getName();
  std::cout << x.getAge();
}

Default Argument

  • Usage: Provide default values for parameters in function declarations.
  • Placement: Typically declared in header or class definition.

Code Example:

File: person.h

#ifndef PERON
#define PERSON
class Person {
  private:
    int age;
    char* name;

  public:
    Person(const char* n, int a);
    ~Person();
    const char* getName() const;
    void setName(const char* n);
    int getAge() const;
    void setAge(int a);
};
#endif

File person.cpp

#include <string.h>
#include <iostream>

#include "person.h"

Person::Person(const char* n = NULL, int a = 0) {
  this->age = a;
  this->name = new char[strlen(n) + 1];
  assert(this->name != 0);
  strcpy(this->name, m);
}

Person::~Person() {
  delete[] this->name;
  this->name = NULL;
}

const char* Person::getName() const {
  return this->name;
}

void Person::setName(const char* n) {
  assert(strlen(n) <= strlen(this->name));
  strcpy(this->name, n);
}

int Person::getAge() const {
  return this->age;
}

void Person::setAge(int a) {
  this->age = a;
}

int main() {
  Person x();

  Person y("Alice");
  std::cout << y.getName();

  Person z("John", 18)
  std::cout << z.getName();
  std::cout << z.getAge();
}

Member Functions with Reference Return Type

  • Reason: Allows direct manipulation of the class's private members without copying.
  • Example: Return a reference to a private member, so it can be changed outside the function.

Code Example:

File: mystring.h

#ifndef MYSTRING
#define MYSTRING
class MyString {
  private:
    int length;
    char* storageM;

  public:
    MyString(const char* s);
    ~MyString();
    const char& at(int i) const;
    char& at(int i);
    const char* getStorageM() const;
    void setStorageM(const char* s);
    int getLength() const;
    void setLength(int l);
};
#endif

File: mystring.cpp

#include <string.h>
#include <iostream>

#include "mystring.h"

MyString::MyString(const char* s) {
  this->length = (int) strlen(s);
  this->storageM = new char[strlen(s) + 1];
  assert(this->storageM != 0);
  strcpy(this->storageM, s);
}

MyString::~MyString() {
  delete[] this->storageM;
  this->storageM = NULL;
}

const char* MyString::getStorageM() const {
  return this->storageM;
}

void MyString::setStorageM(const char* s) {
  assert(strlen(s) <= strlen(this->storageM));
  strcpy(this->storageM, n);
}

int MyString::getLength() const {
  return this->length;
}

void MyString::setLength(int l) {
  this->length = l;
}

const char& MyString::at(int i) const {
  assert(i >= 0 && i < this->length);
  return storageM[i];
}

char& MyString::at(int i) {
  assert(i > 0 && i < this->length);
  return storageM[i];
}

int main() {
  MyString x("Hello World!");
  std::cout << x.at(0);
  std::cout << x.at(6);
}

Member Functions with const Return Type

  • Usage: Return a constant reference or constant value to prevent modification.
  • Example: Returning a const reference to ensure the caller cannot alter the internal data.

Code Example:

File: student.h

#ifndef STUDENT
#define STUDENT
class Student {
  private:
    int idM;
    char nameM[50];

  public:
    Student();
    Student(const char* name, const int id);
    char* getNameMPointer() const;
    const char* getNameMConstPointer() const;
    void setNameM(const char* n);
};
#endif

File: student.cpp

#include <string.h>

#include "student.h"

Student::Student() {
  strcpy(this->nameM, "None");
  this->idM = 0;
}

Student::Student(const char* name, const int id) {
  strcpy(this->nameM, name);
  this->idM = id;
}

char* Student::getNameMPointer() const {
  return this->nameM;
}

const char* Student::getNameMConstPointer() const {
  return this->nameM;
}

void Student::setNameM(const char* n) {
  assert(strlen(n) <= (int) strlen(this->nameM));
  strcpy(this->name, n);
}

int main() {
  char name[] = "Jane";
  Student s(name, 123456);

  char* bad = s.getNameMPointer();
  bad[0] = "P"; // s.nameM is now "Pane"

  const char* good = s.getNameMConstPointer();
  good[0] = "P"; // invalid
}

Inline Member Function

  • Definition: Inlining can be done implicitly or explicitly. A function declared with inline keyword, suggesting the compiler to replace the function call with the function body (optimization hint).
  • Usage: Often used for small, frequently called functions.

Code Example:

File: counter.h

#ifndef COUNTER
#define COUNTER
class Counter {
  private:
    int value;

  public:
    Counter();

    // implicit inline
    void increment(int n) {
      value += n;
    }

    // explicit inline
    inline void decrement(int n) {
      value -= n;
    }
};
#endif

File: counter.cpp

#include "counter.h"

Counter::Counter() : value(0) {}

int main() {
  Counter x;
  x.increment(5);
  x.decrement(6);
}

Copying Objects


Table of Contents


Copy Constructor

  • Definition: A copy constructor is a special constructor used to create a new object as a copy of an existing object.
  • Signature: ClassName(const ClassName& other).
  • Purpose:
    • Enables control over how objects are copied.
    • Helps prevent unwanted shallow copying when the class manages resources (e.g., dynamic memory).

Code Example:

File: mystring.h

#ifndef MYSTRING
#define MYSTRING
class MyString {
  private:
    int lengthM;
    char* storageM;

  public:
    MyString();
    MyString(const char* s);
    Mystring(const MyString& source);
    ~MyString();
    const char* getStorageM() const;
    void setStorageM(const char* s);
    int getLength() const;
    void setLength(int l);
};
#endif

File: mystring.cpp

#include <string.h>
#include <iostream>

#include "mystring.h"

MyString::MyString() : lengthM(0), storageM(new char[1]) {
  this->storageM[0] = "\0";
  std::cout << "default constructor called.";
}

MyString::MyString(const char* s): lengthM((int) strlen(s)) {
  this->storageM = new char[this->lengthM+1];
  assert(this->storageM != 0);
  strcpy(this->storageM, s);
  std::cout << "constructor called.";
}

MyString::MyString(const MyString& s) : lengthM(s.lengthM) {
  this->storageM = new char[this->lengthM + 1];
  assert(this->storageM != 0);
  strcpy(this->storageM, s.storageM);
  std::cout << "copy constructor called.";
}

MyString::~MyString() {
  delete[] this->storageM;
  this->storageM = NULL;
  std::cout << "destructor called.";
}

const char* MyString::getStorageM() const {
  return this->storageM;
}

void MyString::setStorageM(const char* s) {
  assert(strlen(s) <= strlen(this->storageM));
  strcpy(this->storageM, n);
}

int MyString::getLength() const {
  return this->length;
}

void MyString::setLength(int l) {
  this->length = l;
}

int main() {
  MyString s1("World");
  MyString s2 = s1;
}

Overloading Assignment Operator

  • Definition: The assignment operator (operator=) is used to copy the value from one object to another already-existing object.
  • Signature: ClassName& operator=(const ClassName& other);
  • Return Type: It typically returns a reference to the current object (*this) to allow chained assignments (e.g., a = b = c;).
  • Key Considerations:
    • Must handle self-assignment safely (i.e., when this == &other).
    • Ensure correct handling of resources (e.g., deallocate existing memory before allocating new memory to avoid leaks).

Code Example:

File: mystring.h

#ifndef MYSTRING
#define MYSTRING
class MyString {
  private:
    int lengthM;
    char* storageM;

  public:
    MyString();
    MyString(const char* s);
    Mystring(const MyString& source);
    MyString& operator=(MyString& rhs);
    ~MyString();
    const char* getStorageM() const;
    void setStorageM(const char* s);
    int getLength() const;
    void setLength(int l);
};
#endif

File: mystring.cpp

#include <string.h>
#include <iostream>

#include "mystring.h"

MyString::MyString() : lengthM(0), storageM(new char[1]) {
  this->storageM[0] = "\0";
  std::cout << "default constructor called.";
}

MyString::MyString(const char* s): lengthM((int) strlen(s)) {
  this->storageM = new char[this->lengthM + 1];
  assert(this->storageM != 0);
  strcpy(this->storageM, s);
  std::cout << "constructor called.";
}

MyString::MyString(const MyString& source) : lengthM(source.lengthM) {
  this->storageM = new char[this->lengthM+1];
  assert(this->storageM != 0);
  strcpy(this->storageM, source.storageM);
  std::cout << "copy constructor called.";
}

MyString& MyString::operator=(MyString& rhs) {
  if (this != &s) {
    delete[] this->storageM;
    this->lengthM = rhs.lengthM;
    this->storageM = new char[this->lengthM+1];
    assert(this->storageM != NULL);
    strcpy(this->storageM, rhs.storageM);
  }
  std::cout << "assignment operator called.";
  return *this;
}

MyString::~MyString() {
  delete[] this->storageM;
  this->storageM = NULL;
  std::cout << "destructor called.";
}

const char* MyString::getStorageM() const {
  return this->storageM;
}

void MyString::setStorageM(const char* s) {
  assert(strlen(s) <= strlen(this->storageM));
  strcpy(this->storageM, n);
}

int MyString::getLength() const {
  return this->length;
}

void MyString::setLength(int l) {
  this->length = l;
}

int main() {
  MyString s1("World");
  MyString s3("ABC");
  s1 = s3;
}

Linked List


Table of Contents


Introduction

A linked list is a dynamic data structure where each element (commonly called a node) contains data and a pointer (or reference) to the next node in the sequence. Unlike arrays, linked lists do not require contiguous memory space, and their size can grow or shrink at runtime with relative ease.

  • Key Advantages:
    • Dynamic size allocation.
    • Easy insertion/deletion at the beginning or middle of the list.
  • Key Disadvantages:
    • Random access is not possible (traversal is sequential).
    • Extra memory overhead for storing pointers.

Node Structure

A typical singly linked list node in C++ can be represented as a struct or class. Each node holds:

  • A data field (the payload of the node).
  • A pointer to the next node in the list.

Code Example:

File: node.cpp

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

Create Operation

  • The create operation typically involves initializing a head pointer to nullptrptr, indicating an empty list.
  • You may also create an initial node if you want the list to start with some data

Code Example:

File: linkedlist.h

#ifndef LINKEDLIST
#define LINKEDLIST

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

class LinkedList {
  private:
    Node* head;

  public:
    LinkedList();
    ~LinkedList();
};
#endif

File: linkedlist.cpp

#include <iostream>

#include "linkedlist.h"

LinkedList::LinkedList() : head(nullptr) {}

LinkedList::~LinkedList() {
  Node* current = this->head;
  while (current != nullptr) {
    Node* nextNode = current->next;
    delete current;
    current = nextNode;
  }
}

int main() {
  LinkedList myList;
}

Insert Operation

Insertion in a linked list can occur in multiple places:

  • At the head (beginning) of the list.
  • At the tail (end) of the list.
  • After a specified node.

Code Example:

File: linkedlist.h

#ifndef LINKEDLIST
#define LINKEDLIST

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

class LinkedList {
  private:
    Node* head;

  public:
    LinkedList();
    ~LinkedList();
    void insertAtHead(int value);
    void insertAtTail(int value);
    Node* insertAfterValue(int key, int value);
};
#endif

File: linkedlist.cpp

#include <iostream>

#include "linkedlist.h"

LinkedList::LinkedList() : head(nullptr) {}

LinkedList::~LinkedList() {
  Node* current = this->head;
  while (current != nullptr) {
    Node* nextNode = current->next;
    delete current;
    current = nextNode;
  }
}

void LinkedList::insertAtHead(int value) {
  Node* newNode = new Node(value, this->head);
  newNode->next = this->head;
  this->head = newNode;
}

void LinkedList::insertAtTail(int value) {
  Node* newNode = new Node(value);
  if (this->head == nullptr) {
    this->head = newNode;
    return;
  }
  Node* temp = this->head;
  while (temp->next != nullptr) {
    temp = temp->next;
  }
  temp->next = newNode;
}

Node* LinkedList::insertAfterValue(int key, int value) {
  Node* temp = this->head;
  while (temp != nullptr && temp->data != key) {
    temp = temp->next;
  }
  if (temp == nullptr) {
    return nullptr;
  }
  Node* newNode = new Node(value, temp->next);
  temp->next = newNode;
  return newNode;
}

int main() {
  LinkedList myList;
  myList.insertAtHead(5);
  myList.insertAtTail(10);
  myList.insertAfterValue(5, 7);
}

Delete Operation

Deletion can also occur in multiple scenarios:

  • Deleting the first node.
  • Deleting the last node.
  • Deleting a node in the middle (given a specific value or position).

Code Example:

File: linkedlist.h

#ifndef LINKEDLIST
#define LINKEDLIST

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

class LinkedList {
  private:
    Node* head;

  public:
    LinkedList();
    ~LinkedList();
    void insertAtHead(int value);
    void insertAtTail(int value);
    Node* insertAfterValue(int key, int value);
    Node* deleteByValue(int value);
};
#endif

File: linkedlist.cpp

#include <iostream>

#include "linkedlist.h"

LinkedList::LinkedList() : head(nullptr) {}

LinkedList::~LinkedList() {
  Node* current = this->head;
  while (current != nullptr) {
    Node* nextNode = current->next;
    delete current;
    current = nextNode;
  }
}

void LinkedList::insertAtHead(int value) {
  Node* newNode = new Node(value, this->head);
  newNode->next = this->head;
  this->head = newNode;
}

void LinkedList::insertAtTail(int value) {
  Node* newNode = new Node(value);
  if (this->head == nullptr) {
    this->head = newNode;
    return;
  }
  Node* temp = this->head;
  while (temp->next != nullptr) {
    temp = temp->next;
  }
  temp->next = newNode;
}

Node* LinkedList::insertAfterValue(int key, int value) {
  Node* temp = this->head;
  while (temp != nullptr && temp->data != key) {
    temp = temp->next;
  }
  if (temp == nullptr) {
    return nullptr;
  }
  Node* newNode = new Node(value, temp->next);
  temp->next = newNode;
  return newNode;
}

bool LinkedList::deleteByValue(int value) {
  if (this->head == nullptr) {
    return nullptr;
  }

  if (head->data == value) {
    Node* nodeToRemove = head;
    head = head->next;
    nodeToRemove->next = nullptr;
    return nodeToRemove;
  }

  Node* current = this->head;
  while (current->next != nullptr && current->next->data != value) {
    current = current->next;
  }

  if (current->next == nullptr) {
    return nullptr;
  }

  Node* nodeToRemove = current->next;
  current->next = nodeToRemove->next;
  nodeToRemove->next = nullptr;
  return nodeToRemove;
}

int main() {
  LinkedList myList;
  myList.insertAtHead(5);
  myList.insertAtTail(10);
  myList.insertAfterValue(5, 7);

  Node* deleted = myList.deleteByValue(7);
  if(deleted) {
    std::cout << "Deleted: " << deleted->data << std::endl;
    delete deleted; // Free memory for the deleted node.
  } else {
    std::cout << "Value not found for deletion." << std::endl;
  }
}

Traverse Operation

Traversing a linked list means visiting each node from the head to the last node. During traversal, you can:

  • Print node data.
  • Perform checks or calculations on each node.

Code Example:

File: linkedlist.h

#ifndef LINKEDLIST
#define LINKEDLIST

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

class LinkedList {
  private:
    Node* head;

  public:
    LinkedList();
    ~LinkedList();
    void insertAtHead(int value);
    void insertAtTail(int value);
    Node* insertAfterValue(int key, int value);
    Node* deleteByValue(int value);
    void traverse() const;
};
#endif

File: linkedlist.cpp

#include <iostream>

#include "linkedlist.h"

LinkedList::LinkedList() : head(nullptr) {}

LinkedList::~LinkedList() {
  Node* current = this->head;
  while (current != nullptr) {
    Node* nextNode = current->next;
    delete current;
    current = nextNode;
  }
}

void LinkedList::insertAtHead(int value) {
  Node* newNode = new Node(value, this->head);
  newNode->next = this->head;
  this->head = newNode;
}

void LinkedList::insertAtTail(int value) {
  Node* newNode = new Node(value);
  if (this->head == nullptr) {
    this->head = newNode;
    return;
  }
  Node* temp = this->head;
  while (temp->next != nullptr) {
    temp = temp->next;
  }
  temp->next = newNode;
}

Node* LinkedList::insertAfterValue(int key, int value) {
  Node* temp = this->head;
  while (temp != nullptr && temp->data != key) {
    temp = temp->next;
  }
  if (temp == nullptr) {
    return nullptr;
  }
  Node* newNode = new Node(value, temp->next);
  temp->next = newNode;
  return newNode;
}

bool LinkedList::deleteByValue(int value) {
  if (this->head == nullptr) {
    return nullptr;
  }

  if (head->data == value) {
    Node* nodeToRemove = head;
    head = head->next;
    nodeToRemove->next = nullptr;
    return nodeToRemove;
  }

  Node* current = this->head;
  while (current->next != nullptr && current->next->data != value) {
    current = current->next;
  }

  if (current->next == nullptr) {
    return nullptr;
  }

  Node* nodeToRemove = current->next;
  current->next = nodeToRemove->next;
  nodeToRemove->next = nullptr;
  return nodeToRemove;
}

void LinkedList::traverse() const {
  Node* temp = this->head;
  while (temp != nullptr) {
    std::cout << " " << temp->data;
    temp = temp->next;
  }
  std::cout << std::endl;
}

int main() {
  LinkedList myList;
  myList.insertAtHead(5);
  myList.insertAtTail(10);
  myList.insertAfterValue(5, 7);
  myList.traverse();  // Expected output: 5 7 10

  Node* deleted = myList.deleteByValue(7);
  if(deleted) {
    std::cout << "Deleted: " << deleted->data << std::endl;
    delete deleted; // Free memory for the deleted node.
  } else {
    std::cout << "Value not found for deletion." << std::endl;
  }

  myList.traverse();  // Expected output: 5 10
}

Search Operation

Searching involves traversing the list to find a node that matches a given value. If found, you can return a pointer to that node or a boolean indicating success.

Code Example:

File: linkedlist.h

#ifndef LINKEDLIST
#define LINKEDLIST

struct Node {
  int data;
  Node* next;
  Node(int value, Node* nextNode = nullptr) : data(value), next(nextNode) {}
};

class LinkedList {
  private:
    Node* head;

  public:
    LinkedList();
    ~LinkedList();
    void insertAtHead(int value);
    void insertAtTail(int value);
    Node* insertAfterValue(int key, int value);
    Node* deleteByValue(int value);
    void traverse() const;
    Node* search(int key) const;
};
#endif

File: linkedlist.cpp

#include <iostream>

#include "linkedlist.h"

LinkedList::LinkedList() : head(nullptr) {}

LinkedList::~LinkedList() {
  Node* current = this->head;
  while (current != nullptr) {
    Node* nextNode = current->next;
    delete current;
    current = nextNode;
  }
}

void LinkedList::insertAtHead(int value) {
  Node* newNode = new Node(value, this->head);
  newNode->next = this->head;
  this->head = newNode;
}

void LinkedList::insertAtTail(int value) {
  Node* newNode = new Node(value);
  if (this->head == nullptr) {
    this->head = newNode;
    return;
  }
  Node* temp = this->head;
  while (temp->next != nullptr) {
    temp = temp->next;
  }
  temp->next = newNode;
}

Node* LinkedList::insertAfterValue(int key, int value) {
  Node* temp = this->head;
  while (temp != nullptr && temp->data != key) {
    temp = temp->next;
  }
  if (temp == nullptr) {
    return nullptr;
  }
  Node* newNode = new Node(value, temp->next);
  temp->next = newNode;
  return newNode;
}

bool LinkedList::deleteByValue(int value) {
  if (this->head == nullptr) {
    return nullptr;
  }

  if (head->data == value) {
    Node* nodeToRemove = head;
    head = head->next;
    nodeToRemove->next = nullptr;
    return nodeToRemove;
  }

  Node* current = this->head;
  while (current->next != nullptr && current->next->data != value) {
    current = current->next;
  }

  if (current->next == nullptr) {
    return nullptr;
  }

  Node* nodeToRemove = current->next;
  current->next = nodeToRemove->next;
  nodeToRemove->next = nullptr;
  return nodeToRemove;
}

void LinkedList::traverse() const {
  Node* temp = this->head;
  while (temp != nullptr) {
    std::cout << " " << temp->data;
    temp = temp->next;
  }
  std::cout << std::endl;
}

Node* LinkedList::search(int key) const {
  Node* temp = this->head;
  while (temp != nullptr) {
    if (temp->data == key) {
      return temp;
    }
    temp = temp->next;
  }
  return nullptr;
}

int main() {
  LinkedList myList;
  myList.insertAtHead(5);
  myList.insertAtTail(10);
  myList.insertAfterValue(5, 7);
  myList.traverse();  // Expected output: 5 7 10

  Node* found = myList.search(7);
  if(found) {
    std::cout << "Found: " << found->data << std::endl;
  } else {
    std::cout << "Not found." << std::endl;
  }

  Node* deleted = myList.deleteByValue(7);
  if(deleted) {
    std::cout << "Deleted: " << deleted->data << std::endl;
    delete deleted; // Free memory for the deleted node.
  } else {
    std::cout << "Value not found for deletion." << std::endl;
  }

  myList.traverse();  // Expected output: 5 10
}

Pointers


Table of Contents


Introduction

Pointers in C++ are variables that hold the memory addresses of other variables. They allow you to indirectly access and modify the value stored at those addresses.

  • Pointer Declaration:

A pointer is declared by specifying the type of data it will point to, followed by an asterisk (*) and the pointer's name.

Code Example:

File: main.cpp

int *ptr;
  • Pointing:

When you assign the address of a variable to a pointer, you are "pointing" the pointer to that variable. You use the address-of operator (&) for this.

Code Example:

File: main.cpp

int x = 10;
int *ptr = &x;
  • Dereferencing:

Dereferencing a pointer means accessing the value at the memory address stored in the pointer. You use the dereference operator (*) for this.

Code Example:

File: main.cpp

int y = *ptr;

Pointer to Pointer

  • A pointer to pointer is a pointer that stores the address of another pointer rather than storing the address of a variable directly.
  • Syntax: type** ptrToPtr;

Code Example:

File: main.cpp

#include <iostream>

void swapPointers(int **x, int **y) {
  int *temp;
  temp = *x;
  *x = *y;
  *y = temp;
}

int main() {
  int a = 23;
  int b = 40;

  int *p1 = &a;
  int *p2 = &b;

  swapPointers(&p1, &p2);

  std::cout << "p1 points to: " << *p1 << "\n";
  std::cout << "p2 points to: " << *p2 << "\n";
}

Array of Pointer

An array of pointers is simply an array whose elements are pointer variables. Useful when you want to store multiple addresses, e.g., addresses of different variables or the starting addresses of multiple strings.

Code Example:

File: main.cpp

#include <iostream>

int main(int argc, char **argv) {
  const char *p[3];
  p[0] = "XYZ";
  p[1] = "KLM";
  p[2] = "ABC";

  std::cout << p[1] << std::endl; // p[1] is pointer to "KLM"
  std::cout << *p[1] << std::endl; // *p1[1] is a pointer to "K"
  std::cout << **p << std::endl; // **p is a pointer to "X"
  std::cout << *(p+1) << std::endl; // *(p+1) is a pointer to "KLM"
}

Arguments of main Function

  • argc: The number of command-line arguments (argument count).
  • argv: An array of C-style strings (argument vector). Each element in argv is a pointer to a character array representing a command-line argument.

Code Example:

File: main.cpp

#include <iostream>

int main(int argc, char **argv) {
  for (int i = 0; i < argc; i++) {
    std::cout << "Argument " << i << ": " << argv[i] << std::endl;
  }
}

Command Line Interface:

./example.exe cat cow dog

Argument 0: ./example.exe
Argument 1: cat
Argument 2: cow
Argument 3: dog

Static Members and Friends


Table of Contents


Static Data Members

  1. Single Instance: A static data member is allocated only once in memory, regardless of how many objects of that class are created.

  2. Initialization: Must be defined and initialized outside the class definition to allocate storage.

  3. Access:

    • Through the class name: ClassName::staticMember.
    • Through an instance (not recommended, but allowed): objectName.staticMember.

Code Example:

File: counter.h

#ifndef COUNTER
#define COUNTER
class Counter {
  private:
    static int count;

  public:
    Counter();
};
#endif

File: counter.cpp

#include "counter.h"

Counter::Counter() {
  count++;
}

int Counter::count = 0;

int main() {
  Counter c1;
}

Static Member Functions

  1. No this Pointer: Static member functions cannot access non-static data members directly because they do not have an implicit this pointer.
  2. Class-Level Operations: Typically used for utility functions that affect the class as a whole (e.g., counting instances, maintaining global state).
  3. Access:
  • Through the class name: ClassName::staticFunction().
  • Through an instance: objectName.staticFunction() (though using the class name is clearer).

Code Example:

File: counter.h

#ifndef COUNTER
#define COUNTER
class Counter {
  private:
    static int count;

  public:
    Counter();
    static void showCount();
};
#endif

File: counter.cpp

#include <iostream>

#include "counter.h"

Counter::Counter() {
  count++;
}

void Counter::showCount() {
  std::cout << "Number of Counter objects: " << count << std::endl;
}

int Counter::count = 0;

int main() {
  Counter c1;
  Counter c2;

  Counter::showCount(); // Output: Number of Counter objects: 2

  Counter c3;
  Counter::showCount(); // Output: Number of Counter objects: 3
}

Friend Functions

  • Definition: A friend function is a non-member function that has access to the private and protected members of a class.
  • Declaration: Declared with the friend keyword inside the class.
  • Advantages: Allows certain external functions to have intimate knowledge of a class’s internals without making those internals public.
  • Limitations: Does not violate encapsulation if used judiciously, but can reduce maintainability if overused.

Code Example:

File: box.h

#ifndef BOX
#define BOX
class Box {
  private:
    double length;
    double width;
    double height;
    friend double getVolume(const Box&);

  public:
    Box(double l, double w, double h)
};
#endif

File: box.cpp

#include <iostream>

#include "box.h"

Box::Box(double l, double w, double h) : length(l), width(w), height(h) {}

double getVolume(const Box& b) {
  return b.length * b.width * b.height;
}

int main() {
  Box box(3.0, 4.0, 5.0);

  std::cout << "Volume: " << getVolume(box) << std::endl;
}

Friend Classes

  • Definition: One class can be declared as a friend of another, giving it access to the friend class’s private and protected members.
  • Usage:
    • Useful when two or more classes need to cooperate closely, sharing implementation details.
    • Should be used sparingly to avoid excessive coupling.

Code Example:

File: alpha.h

#ifndef ALPHA
#define ALPHA
class Alpha;

class Alpha {
  private:
    int data;
    friend class Beta;

  public:
    Alpha(int value);
};
#endif

File: beta.h

#ifndef BETA
#define BETA
class Beta {
  private:
    int data;

  public:
    Beta(int value);
    void showAlphaData(const Alpha& a);
};
#endif

File: main.cpp

#include <iostream>

#include "alpha.h"
#include "beta.h"

Alpha::Alpha(int value) : data(value) {}

Beta::Beta(int value) : data(value) {}

void Beta::showAlphaData(const Alpha& a) {
    std::cout << "Alpha's data = " << a.data << std::endl;
}

int main() {
    Alpha alpha(42);
    Beta beta;
    beta.showAlphaData(alpha);
}

Overloading Operators


Table of Contents


Overloading +

Concatenate two String objects and return the result as a temporary.
The left-hand and right-hand operands remain unchanged.

File string.h:

class String {
    public:
        ...
        String operator +(const String& s);

    private:
        char* storage_;
        int length_;
};

File string.cpp:

String String::operator +(const String& s) {
    String tmp;
    tmp.length_ = length_ + s.length_;
    tmp.storage_ = new char[tmp.length_ + 1];

    std::strcpy(tmp.storage_, storage_);
    std::strcat(tmp.storage_, s.storage_);

    return tmp;
}

File main.cpp:

int main() {
    String s1("Hello, ");
    String s2("World!");
    String s3;
    s3 = s1 + s2;
    return 0;
}

Overloading +=

Modify the current object in place by appending another String.

File string.h:

class String {
    public:
        ...
        String& operator +=(const String& s);

    private:
        char* storage_;
        int length_;
};

File string.cpp:

String& String::operator +=(const String& s) {
    length_ += s.length_;
    char* new_storage = new char[length_ + 1];
    assert(new_storage != nullptr);

    std::strcpy(new_storage, storage_);
    std::strcat(new_storage, s.storage_);

    delete[] storage_;
    storage_ = new_storage;
    return *this;
}

File main.cpp:

int main() {
    String s1("Hello, ");
    String s2("World!");
    s1 += s2;
    return 0;
}

Overloading <<

Stream the String to an output stream.

File string.h:

class String {
    public:
        ...
        // declare as friend to allow access to private members
        friend std::ostream& operator<<(std::ostream& os, const String& s);

    private:
        char* storage_;
        int length_;
};

File string.cpp:

std::ostream& operator<<(std::ostream& os, const String& s) {
    return os << s.storage_;
}

File main.cpp:

int main() {
    String s1("Hello, ");
    String s2("World!");
    cout << s1 << s2 << endl; // prints "Hello, World!"
    return 0;
}

Overloading >>

Read characters from an input stream into a String.

File string.h:

class String {
    public:
        ...
        // declare as friend to allow access to private members
        friend std::istream& operator>>(std::istream& is, String& s);

    private:
        char* storage_;
        int length_;
};

File string.cpp:

std::istream& operator>>(std::istream& is, String& s) {
    return is >> s.storage_;
}

File main.cpp:

int main() {
    String s1;
    cout << "Enter a string: ";
    cin >> s1;
    return 0;
}

Overloading []

Provide direct (bounds-checked) character access.

File string.h:

class String {
    public:
        ...
        char& operator[](int index);

    private:
        char* storage_;
        int length_;
};

File string.cpp:

char& String::operator[](int index) {
    assert(index >= 0 && index < length_);
    return storage_[index];
}

File main.cpp:

int main() {
    String s1("Hello, World!");
    cout << s1[0] << endl; // prints 'H'
    return 0;
}

Overloading ++

Increment the first character; prefix returns the new value, postfix the old.

File string.h:

class String {
    public:
        ...
        char operator++();     // prefix
        char operator++(int);  // postfix

    private:
        char* storage_;
        int length_;
};

File string.cpp:

char String::operator++() {
    return ++storage_[0];        // mutate then return
}

char String::operator++(int) {
    char tmp = storage_[0];
    storage_[0]++;
    return tmp;                 // return original value
}

File main.cpp:

int main() {
    String s1("Hello, World!");
    cout << ++s1 << endl; // prefix increment
    cout << s1++ << endl; // postfix increment
    return 0;
}

Overloading --

Decrement the first character; mirrors the semantics of ++.

File string.h:

class String {
    public:
        ...
        char operator--();     // prefix
        char operator--(int);  // postfix

    private:
        char* storage_;
        int length_;
};

File string.cpp:

char String::operator--() {
    return --storage_[0];
}

char String::operator--(int) {
    char tmp = storage_[0];
    storage_[0]--;
    return tmnp;
}

File main.cpp:

int main() {
    String s1("Hello, World!");
    cout << --s1 << endl; // prefix decrement
    cout << s1-- << endl; // postfix decrement
    return 0;
}

Credits

This project was developed by Axel Omar Sánchez Peralta.

  • Role: Software Engineering Student at the University of Calgary
  • Current Position: Cloud Developer Intern at Aptum

Feel free to connect with me through the following channels:

Thank you for your interest in this project!

                             Apache License
                       Version 2.0, January 2004
                    http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

  1. Definitions.

    "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

    "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

    "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

    "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

    "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

    "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

    "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

    "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

    "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

    "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

  2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

  3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

  4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

    (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

    (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

    (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

    (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

  5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

  6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

  7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

  8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

  9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

  To apply the Apache License to your work, attach the following
  boilerplate notice, with the fields enclosed by brackets "[]"
  replaced with your own identifying information. (Don't include
  the brackets!)  The text should be enclosed in the appropriate
  comment syntax for the file format. We also recommend that a
  file or class name and description of purpose be included on the
  same "printed page" as the copyright notice for easier
  identification within third-party archives.

Copyright 2025 Axel Omar Sanchez Peralta

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.